[워밍업 클럽 스터디 2기 - BE] (클린코드, 테스트코드) day 18 미션

[워밍업 클럽 스터디 2기 - BE] (클린코드, 테스트코드) day 18 미션

출처 : 인프런 워밍업 클럽 스터디 2기 - 백엔드 클린코드, 테스트 코드(Java, Spring Boot)

Practical Testing: 실용적인 테스트 가이드

 

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()을 테스트를 할 때 mailSendClientsendMail()만 가짜로 쓴다는 의미이다.

 

@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  
    검증  
}

 

 

댓글을 작성해보세요.

채널톡 아이콘