mockito 사용법(mockito usage) :: JDM's Blog
Mockito features in Korean
public interface OrederService {
ResponseDTO findById(Long id);
ResponseDTO createOrder(RequestDTO request);
}
@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class OrederService {
private final OrederRepository orderRepository;
public ResponseDTO findById(Long id){
Order order = orderRepository.findById(id);
// ResponseDTO내의 entity를 DTO로 변환해서 반환하는 static 메서드
return ResponseDTO.fromEntity(order);
}
/**
해당 작업이 데이터 생성과 관련된 작업이기 때문에 @Transactional(readOnly=true)가
아닌 @Transational 애너테이션이 사용되어야 한다.
*/
@Transactional // 해당 메서드 레벨의 애너테이션이 더 우선순위로 동작한다.
public ResponseDTO createOrder(RequestDTO request){
Order order = orderRepository.findByOrderId(request.getId());
//해당 아이디에 해당하는 주문 객체가 없으면 생성가능
if(Objects.isNull(order)){
Oder newOrder = Order.createOrder(request);
orderRepository.save(newOrder);
return ResponseDTO.fromEntity(newOrder);
}
throw new AlreadyOrderException("이미 해당 주문이 존재합니다.");
}
}
@Service
@Bean
, @Component
등을 조금 더 구체적으로 선언하고자 할 때 @Service
라고 쓰는 용도예요.@Transactional
@Transactional
애너테이션을 해당 메서드 레벨에 붙여주면 돼요.@Transaction
애너테이션을 붙여주게 되면 우선순위가 메서드→클래스 순서라 클래스 레벨에 붙어 있는 속성은 덮어지게 돼요.단위테스트를 잘 짜기 위한 원칙으로 로버트 C. 마틴의 클린코드에서 확인할 수 있어요.
테스트는 빠르게 진행되어야 해요.
각 테스트는 서로 의존적이지 않아야 해요. 독립적으로 진행 해야 하고 어떤 순서로 실행해도 잘 실행되어야 해요. 테스트가 의존적으로 진행될 경우에 하나의 테스트가 실패했을 때 나머지도 잇달아 실패하게 되기 때문에 원인을 찾아내기 힘들어지고 후반 테스트 작업에 까지 영향을 미칠 수 있어서 꼭 독립적으로 진행 되어야 해요.
테스트 코드를 작성했다면 해당 테스트 코드는 어떤 환경에서든 동일하게 동작해야 해요.
즉 실제 환경, QA 환경, 버스를 타고 가는 집으로 가는 길에 사용하는 노트북 환경에서도 실행할 수 있어야 해요.
반복 가능한 테스트는 외부 서비스나 리소스에 의존하지 않고 테스트하는 것을 의미해요. 네트워크, 개발 서버의 네트워크 환경에 상관 없이 실행되어야 하고, 단위 테스트는 외부 시스템을 테스트하지 않아야 해요.
테스트는 boolean 값으로 결과를 내야 해요. 즉, 이말은 성공 아니면 실패로 결과를 내야 한다는 것을 의미해요.
테스트 코드 작성은 기능 구현 이전에 이뤄져야 해요. 이 작업이 실제 코드 작성 후에 테스트 코드가 작성되면 실제 코드가 테스트 하기 어려워진다는 사실을 발견할 수 있어요. 그래서 테스트 작업은 대부분 기능과 관련된 코드의 작성 이전에 이뤄져야 해요.
Person 객체
public class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
Mock 객체를 이용한 테스트 코드
@Test
public void example(){
Person p = mock(Person.class);
assertTrue( p != null );
}
@Mock 애너테이션 사용을 이용한 테스트 코드
@Mock
Person person;
@Test
public void example1(){
MockitoAnnotations.initMocks(this);
assertTrue(person != null);
}
@Test
public void example(){
Person p = mock(Person.class);
when(p.getName()).thenReturn("JDM");
when(p.getAge()).thenReturn(20);
assertTrue("JDM".equals(p.getName()));
assertTrue(20 == p.getAge());
}
@Test(expected = IllegalArgumentException.class)
public void example(){
Person p = mock(Person.class);
doThrow(new IllegalArgumentException()).when(p).setName(eq("JDM"));
String name = "JDM";
p.setName(name);
}
@Test
public void example(){
Person p = mock(Person.class);
doNothing().when(p).setAge(anyInt());
p.setAge(20);
verify(p).setAge(anyInt());
}
@Test
public void example(){
Person p = mock(Person.class);
String name = "JDM";
p.setName(name);
// n번 호출했는지 체크
verify(p, times(1)).setName(any(String.class)); // success
// 호출 안했는지 체크
verify(p, never()).getName(); // success
verify(p, never()).setName(eq("ETC")); // success
verify(p, never()).setName(eq("JDM")); // fail
// 최소한 1번 이상 호출했는지 체크
verify(p, atLeastOnce()).setName(any(String.class)); // success
// 2번 이하 호출 했는지 체크
verify(p, atMost(2)).setName(any(String.class)); // success
// 2번 이상 호출 했는지 체크
verify(p, atLeast(2)).setName(any(String.class)); // fail
// 지정된 시간(millis)안으로 메소드를 호출 했는지 체크
verify(p, timeout(100)).setName(any(String.class)); // success
// 지정된 시간(millis)안으로 1번 이상 메소드를 호출 했는지 체크
verify(p, timeout(100).atLeast(1)).setName(any(String.class)); // success
}
AuthService 코드
public class AuthService{
private AuthDao dao;
// some code...
public boolean isLogin(String id){
boolean isLogin = dao.isLogin(id);
if( isLogin ){
// some code...
}
return isLogin;
}
}
public class AuthDao {
public boolean isLogin(String id){ //some code ... }
}
public class AuthService{
private AuthDao dao;
}
@InjectMocks
를 사용해요.테스트 코드
@Mock
AuthDao dao;
@InjectMocks
AuthService service;
@Test
public void example(){
MockitoAnnotations.initMocks(this);
when(dao.isLogin(eq("JDM"))).thenReturn(true);
assertTrue(service.isLogin("JDM") == true);
assertTrue(service.isLogin("ETC") == false);
}
서비스 계층의 테스트를 진행할 때 가장 중요한 것은 영속성 계층의 연결 없이도 테스트가 가능해야 한다는 사실이에요. 이에 주의하고 서비스 계층의 테스트를 진행해봅시다.
이때 서비스 계층에서도 통합 테스트가 아닌 단위 테스트를 진행할 거예요. 영속성 계층에서 슬라이스 테스트를 진행하였는데, 서비스 계층에선 따로 그와 관련된 작업은 없어요. 그래서 주로 Mockito와 같은 목 객체(Mock)를 사용하여 의존성을 가짜 객체로 대체하고, 서비스 계층의 메소드들이 예상대로 작동하는지를 검증하는 식으로 진행이 돼요.
@SpringBootTest
class ArticleServiceTest {
@Autowired
ArticleService articleService;
private Article article;
@BeforeEach
void setUp() {
article = Article.builder()
.contents("contents")
.coverUrl("coverUrl")
.title("title")
.build();
article = articleService.save(article);
}
@Test
void 게시글_조회() {
assertThat(articleService.findById(article.getId())).isNotNull();
}
@Test
void 존재하지_않는_게시글_조회_예외처리() {
assertThrows(IllegalArgumentException.class, () -> articleService.findById(100L));
}
...
@SpringBootTest
@Service
@Transactional
public class ArticleService {
private final UserService userService;
private final ArticleRepository articleRepository;
public Long save(final Long userId, final ArticleDto.Request articleDto) {
User author = userService.findById(userId);
Article article = articleDto.toArticle(author);
return articleRepository.save(article).getId();
}
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ArticleServiceTest {
private static final Long ARTICLE_ID = 1L;
private static final Long USER_ID = 1L;
@InjectMocks
private ArticleService articleService;
@Mock
private UserService userService;
@Mock
private ArticleRepository articleRepository;
private User user;
private ArticleDto.Request articleRequest;
private Article article;
@BeforeEach
void setUp() {
user = new User(USER_ID, "email@gamil.com", "name", "P@ssw0rd");
articleRequest = new ArticleDto.Request(ARTICLE_ID, "contents", "title", "coverUrl");
article = articleRequest.toArticle(user);
}
@Test
void 게시글_저장() {
// given
when(userService.findById(USER_ID)).thenReturn(user);
when(articleRepository.save(article)).thenReturn(article);
// when
articleService.save(USER_ID, articleRequest);
// then
verify(articleRepository).save(article); // articleRepository.save(article)가 호출되었는지 확인
}
}
@SpringBootTest
when()
when(userService.findById(USER_ID)).thenReturn(user);
: UserService의 findById
메서드가 USER_ID
를 전달받을 때, 가짜 구현으로서 user
객체를 반환하도록 해요.when(articleRepository.save(article)).thenReturn(article);
: ArticleRepository의 save
메서드가 article
객체를 전달받을 때, 가짜 구현으로서 같은 article
객체를 반환하도록 지정해요@InjectMocks
@Mock