[워밍업 클럽 스터디 2기 - BE] (클린코드, 테스트코드) day 18 미션
출처 : 인프런 워밍업 클럽 스터디 2기 - 백엔드 클린코드, 테스트 코드(Java, Spring Boot)
1. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks
이 애노테이션들을 한번 정리하고 차이점을 알아보자.
@Mock
org.mockito.Mock
이며 Mockito.mock()
을 애노테이션화 한 것으로 볼 수 있다.
사용하려면 junit5
기준으로 @ExtendWith(MockitoExtension.class)
을 클래스 위에 달아주어야 한다.
역할은 내가 어떤 테스트를 하고 싶은데, 그 테스트를 하려면 또 다른 의존성(클래스)를 끌고 와야 하는 상황에서 이 의존성을 가짜로 바꿔치기 (Stub
)해 주는 것이다.
그래서 내가 테스트 하고 싶은 코드만 테스트 할 수 있게 되는 것이다.
@MockBean
org.springframework.boot.test.mock.mockito.MockBean
패키지이며(!) spring-boot-test
가 제공하는 애노테이션 이다.
이걸 사용하면 스프링 컨텍스트가 관리하는 빈을 대체할 수 있다. → 즉 @SpringBootTest
를 사용해야함.
mock객체를 스프링 컨텍스트에 대신 등록하는 것이다. 그래서 Autowired에 의존성이 주입됨.
역할은 @Mock
과 유사하다.
@InjectMocks
org.mockito.InjectMocks
이며 @Mock
이나 @Spy
가 붙은 의존성 객체를 내가 테스트 할 객체에 주입할 때 사용하는 애노테이션이다.
@Mock
private MailSendClient mailSendClient; // 의존성(가짜 객체)
@InjectMocks
private MailService mailService; //실제 테스트할 객체
코드에서 보면 mailSendClient
라는 가짜 객체를 테스트를 위해 mailService
에 주입하겠다는 것이다.
@Spy
org.mockito.Spy
이며 Mockito.spy()
를 애노테이션화 한 것으로 볼 수 있다.
의존성을 대체하는 @Mock
과 같은 역할을 하지만 한 가지 큰 차이점이 있다.
@Spy
를 사용하면 일부 기능은 가짜로 Stub
할 수 있고, 일부 기능은 실제로 동작 시킬 수 있다.
@Slf4j
@Component
public class MailSendClient {
// 메일 전송
public boolean sendEmail(String fromEmail, String toEmail, String subject, String content) {
log.info("메일 전송");
throw new IllegalArgumentException("메일 전송");
}
public void a() {
log.info("a");
}
public void b() {
log.info("b");
}
public void c() {
log.info("c");
}
}
다음과 같은 가짜로 대체하고 싶은 클래스가 있다. 여기서 sendEmail()
만 가짜로 Stub
하고 나머지 a()
, b()
, c()
는 실제 메서드를 실행하고 싶은 거다.
public class MailService {
private final MailSendClient mailSendClient;
public boolean sendMail(String fromEmail, String toEmail, String subject, String content) {
boolean result = mailSendClient.sendEmail(fromEmail, toEmail, subject, content);
if (result) {
// 추가 로직
}
mailSendClient.a();
mailSendClient.b();
mailSendClient.c();
...
}
}
이런 식으로 MailService
에서 sendMail()
을 테스트를 할 때 mailSendClient
의 sendMail()
만 가짜로 쓴다는 의미이다.
@Spy
private MailSendClient mailSendClient;
@InjectMocks
private MailService mailService;
@Test
@DisplayName("메일 전송 테스트")
void sendMail() {
doReturn(true)
.when(mailSendClient)
.sendEmail(anyString(), anyString(), anyString(), anyString());
boolean result = mailService.sendMail("", "", "", "");
}
이렇게 doReturn().when().스터빙할메서드()
이라는 메서드 체이닝을 통해 스터빙할 수 있다.
@Spy
를 사용하면 일부 기능은 가짜로Stub
할 수 있고, 일부 기능은 실제로 동작 시킬 수 있다.
@SpyBean
이것도 @MockBean
과 마찬가지로 org.springframework.boot.test.mock.mockito.SpyBean
spring-boot-test
가 제공하는 애노테이션 이다. 즉 @SpringBootTest
를 사용해야함.
가짜 객체의 일부만 stub하고, 나머지는 실제 메서드를 사용하고 싶을 때 사용.
2. 다음과 같은 테스트가 있을 때 이걸 분류해보자.
@BeforeEach void setUp() {
❓
}
@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
1-1. 사용자 생성에 필요한 내용 준비
1-2. 사용자 생성
1-3. 게시물 생성에 필요한 내용 준비
1-4. 게시물 생성
1-5. 댓글 생성에 필요한 내용 준비
1-6. 댓글 생성
// given
❓
// when
❓
// then
검증
}
@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
2-1. 사용자 생성에 필요한 내용 준비
2-2. 사용자 생성
2-3. 게시물 생성에 필요한 내용 준비
2-4. 게시물 생성
2-5. 댓글 생성에 필요한 내용 준비
2-6. 댓글 생성
2-7. 댓글 수정
// given
❓
// when
❓
// then
검증
}
@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
3-1. 사용자1 생성에 필요한 내용 준비
3-2. 사용자1 생성
3-3. 사용자2 생성에 필요한 내용 준비
3-4. 사용자2 생성
3-5. 사용자1의 게시물 생성에 필요한 내용 준비
3-6. 사용자1의 게시물 생성
3-7. 사용자1의 댓글 생성에 필요한 내용 준비
3-8. 사용자1의 댓글 생성
3-9. 사용자2가 사용자1의 댓글 수정 시도
// given
❓
// when
❓
// then
검증
}
여기서 @DisplayName
으로 뭘 테스트할지 알 수 있다.
이번 테스트는 댓글
에 대한 테스트를 하려는 것으로 느낄 수 있다.
그럼 given
절에는 댓글을 달기 위한 사전 준비를 해야 하는데,
반복적으로 준비해야 하는 것은 setUp()
으로 빼보자.
1-1. 사용자 생성에 필요한 내용 준비
1-2. 사용자 생성
1-3. 게시물 생성에 필요한 내용 준비
1-4. 게시물 생성
2-1. 사용자 생성에 필요한 내용 준비
2-2. 사용자 생성
2-3. 게시물 생성에 필요한 내용 준비
2-4. 게시물 생성
3-1. 사용자1 생성에 필요한 내용 준비
3-2. 사용자1 생성
3-5. 사용자1의 게시물 생성에 필요한 내용 준비
3-6. 사용자1의 게시물 생성
이 내용은 계속 반복된다.
@BeforeEach
void setUp() {
사용자 생성에 필요한 내용 준비
사용자 생성
게시물 생성에 필요한 내용 준비
게시물 생성
}
이렇게 준비해보자.
첫 번째 테스트는 댓글을 작성할 수 있다
가 핵심이기 때문에 다음과 같이 나타냈다.
@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
// given
1-5. 댓글 생성에 필요한 내용 준비
// when
1-6. 댓글 생성
// then
검증
}
두 번째 테스트는 댓글을 수정
이 핵심이기 때문에 댓글 생성까지는 준비하고, 핵심인 댓글 수정
을 when에서 실행했다.
@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
// given
2-5. 댓글 생성에 필요한 내용 준비
2-6. 댓글 생성
// when
2-7. 댓글 수정
// then
검증
}
마지막 테스트는 타인이 댓글을 수정 시도한다.
이기 때문에
given절에선 타인
사용자 2를 만들어주고, 내가 댓글을 생성해 놓는다.
그리고 when절에서 타인
이 내 댓글
을 수정
시도한다.
@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
// given
3-3. 사용자2 생성에 필요한 내용 준비
3-4. 사용자2 생성
3-7. 사용자1의 댓글 생성에 필요한 내용 준비
3-8. 사용자1의 댓글 생성
// when
3-9. 사용자2가 사용자1의 댓글 수정 시도
// then
검증
}
전체 코드는 다음과 같다.
@BeforeEach
void setUp() {
사용자 생성에 필요한 내용 준비
사용자 생성
게시물 생성에 필요한 내용 준비
게시물 생성
}
@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
// given
1-5. 댓글 생성에 필요한 내용 준비
// when
1-6. 댓글 생성
// then
검증
}
@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
// given
2-5. 댓글 생성에 필요한 내용 준비
2-6. 댓글 생성
// when
2-7. 댓글 수정
// then
검증
}
@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
// given
3-3. 사용자2 생성에 필요한 내용 준비
3-4. 사용자2 생성
3-7. 사용자1의 댓글 생성에 필요한 내용 준비
3-8. 사용자1의 댓글 생성
// when
3-9. 사용자2가 사용자1의 댓글 수정 시도
// then
검증
}
댓글을 작성해보세요.