이번 포스트는 Mock에 대한 심도높은 이해를 탐구하는 내용이다.
1. Mock이 뭘까?
- “Mock"이라는 단어는 테스트에서 많이 사용되는 용어로, 실제 객체를 모방한 가짜 객체를 의미한다.
- 이러한 Mock 객체는 테스트를 수행하는 동안 실제 객체의 행동을 흉내내거나 예측된 방식으로 반응하도록 설정할 수 있다. 이를 통해 테스트를 보다 통제 가능한 환경에서 실행할 수 있다.
- MockMVC:
- Spring MVC의 동작을 모방(Mock)하는 라이브러리다.
- 이것을 사용하면 HTTP 요청을 DispatcherServlet에 보내고 그 결과를 검증할 수 있다.
- 실제 네트워크를 통하지 않고도 Spring MVC의 동작을 테스트할 수 있어, 테스트 속도가 빠르고 어떤 Controller와 View가 호출되었는지, 어떤 상태 코드와 헤더가 반환되었는지 등을 확인할 수 있다.
- @InjectMocks:
- Mockito라는 테스팅 프레임워크에서 제공하는 어노테이션으로, 테스트 대상 클래스에 Mock 객체를 주입할 때 사용한다.
- 이 어노테이션을 사용하면 테스트 대상 클래스의 인스턴스를 생성하고, 그 안에 필요한 의존성을 Mock 객체로 자동 주입해준다.
- MockMVC:
요약하면, Mocking은 테스트를 보다 통제 가능하고 예측 가능한 환경에서 실행하기 위한 기법이다.
MockMVC, @InjectMocks, MockConfiguration 등은 이러한 Mocking을 쉽게 구현할 수 있도록 도와주는 도구들이다.
2. 왜 테스트에선 Mock 객체를 사용할까?
- Mock 객체를 사용하는 주요한 이유는 테스트를 보다 효율적이고, 견고하게 만들기 위함이다.
- 몇 가지 이유를 좀 더 자세히 살펴보겠다.
- 테스트 속도 향상:
- 실제 객체를 사용하면 데이터베이스, 네트워크, 파일 시스템 등과 같은 외부 자원에 접근해야 할 수 있다.
- 이러한 작업은 시간이 많이 걸리며, 테스트의 속도를 느리게 만든다. 반면에, Mock 객체는 메모리 내에서 동작하기 때문에 테스트의 속도를 크게 향상시킬 수 있다.
- 테스트의 견고성 향상:
- 실제 객체를 사용하면 외부 환경의 변화에 따라 테스트 결과가 달라질 수 있다.
- 예를 들어, 데이터베이스에 데이터가 변경되거나, 네트워크 연결이 끊어지는 등의 문제가 발생하면 테스트가 실패할 수 있다. 반면에, Mock 객체는 항상 동일한 동작을 보장하기 때문에 테스트의 견고성을 향상시킬 수 있다.
- 테스트의 단순화:
- 실제 객체를 사용하면 복잡한 초기화 과정이 필요할 수 있다.
- 예를 들어, 데이터베이스에 테스트 데이터를 삽입하거나, 네트워크 서버를 설정하는 등의 작업이 필요할 수 있다. 반면에, Mock 객체는 간단한 설정만으로 원하는 동작을 설정할 수 있다.
- 테스트의 명확성 향상:
- Mock 객체를 사용하면 테스트 대상 코드와 그 외의 코드를 명확하게 분리할 수 있다.
- 이는 테스트의 목적을 명확하게 하고, 테스트 코드를 이해하기 쉽게 만든다.
- 테스트 속도 향상:
따라서, Mock 객체는 테스트의 효율성, 견고성, 단순성, 명확성을 향상시키는 데 도움이 된다.
3. Spring Boot에서 @MockBean객체를 사용한 테스트 코드를 작성해보자
- 컨트롤러 작성
@RestController
public class MyController {
@Autowired
private MyService myService;
@GetMapping("/items/count")
public String getItemsCount() {
long count = myService.countItems();
return String.valueOf(count);
}
}
- 서비스 작성
@Service
public class MyService {
// 실제로는 데이터베이스나 다른 외부 자원과 상호작용하는 코드가 있을 수 있습니다.
// 이 예시에서는 단순히 123L을 반환하도록 합니다.
public long countItems() {
return 123L;
}
}
- 테스트 코드 작성
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MyService myService;
@Test
public void whenTestController_thenCorrect() throws Exception {
Mockito.when(myService.countItems()).thenReturn(123L);
mockMvc.perform(get("/items/count"))
.andExpect(status().isOk())
.andExpect(content().string("123"));
}
}
- 위의 테스트 코드에서, @MockBean 어노테이션을 사용하여 MyService의 인스턴스 myService를 Mock 객체로 생성했다. 그리고 @AutoConfigureMockMvc 어노테이션을 사용하여 MockMvc 인스턴스 mockMvc를 주입받았다.
- 이제 mockMvc.perform(get("/items/count"))를 호출하면 MyController의 getItemsCount 메소드가 호출된다. 그리고 myService.countItems()의 동작을 Mockito.when(myService.countItems()).thenReturn(123L);을 통해 123L을 반환하도록 변경했다. 따라서 andExpect(content().string("123"));는 통과한다.
- 이렇게 @MockBean과 @AutoConfigureMockMvc를 사용하면, 실제 Spring MVC 컨트롤러를 Mock 객체와 함께 테스트할 수 있다. 이는 테스트의 복잡성을 줄이고, 테스트의 목적에 집중할 수 있도록 도와준다.
4. @Mock을 사용한 테스트 코드 작성
- @Mock 어노테이션은 Mockito 프레임워크에서 제공하는 기능으로, 해당 필드를 Mock 객체로 만든다.
- Mock 객체는 원래 객체와 동일한 타입이지만, 그 행동은 테스트 케이스에서 정의한 대로 동작한다. 이를 통해 특정 메소드가 호출되었을 때 어떤 값을 반환할지, 어떤 예외를 던질지 등을 설정할 수 있다.
리포지토리 코드 작성
@Repository
public interface MyRepository extends JpaRepository<MyEntity, Long> {
}
서비스 코드 작성
- 여기서 MyService의 countItems 메소드가 MyRepository의 count 메소드를 어떻게 호출하는지 테스트하려고 한다.
- 그러나 MyRepository의 실제 동작을 테스트하려는 것이 아니라, MyService가 MyRepository를 어떻게 사용하는지만 테스트하려는 것이다. 이럴 때 @Mock을 사용할 수 있다.
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
public long countItems() {
return myRepository.count();
}
}
테스트 코드 작성
@SpringBootTest
public class MyServiceTest {
@Mock
private MyRepository myRepository;
@InjectMocks
@Autowired
private MyService myService;
@Test
public void whenUseMockAnnotation_thenCorrect() {
//여기서 mock의 동작을 설정한다.
Mockito.when(myRepository.count()).thenReturn(123L);
long count = myService.countItems();
assertEquals(123L, count);
}
}
- 위의 테스트 코드에서, @Mock 어노테이션을 사용하여 MyRepository의 인스턴스 myRepository를 Mock 객체로 생성했다. 그리고 @InjectMocks 어노테이션을 사용하여 MyService의 인스턴스 myService를 생성하고 myRepository를 주입했다.
- 이제 myService.countItems()를 호출하게 되면 내부적으로 myRepository.count()가 호출된다. 그리고 myRepository.count()의 동작을 Mockito.when(myRepository.count()).thenReturn(123L);을 통해 123L을 반환하도록 변경했다. 따라서 assertEquals(123L, count);는 통과한다.
- 이렇게 @Mock을 사용하면, 테스트 대상 클래스가 의존하는 다른 클래스를 Mock 객체로 대체하여 테스트 대상 클래스만을 고립시킨 상태에서 테스트를 수행할 수 있다. 이는 테스트의 복잡성을 줄이고, 테스트의 목적에 집중할 수 있도록 도와준다.
5. @InjectMock 이란
- @InjectMocks 어노테이션은 Mockito에서 제공하는 기능으로, 테스트하려는 클래스의 인스턴스를 생성하고 해당 클래스의 필드 중 @Mock으로 표시된 필드를 자동으로 주입한다. 이를 통해 테스트 대상 클래스를 실제 환경과 유사한 상태로 만들 수 있다.
- @InjectMocks 어노테이션은 테스트 클래스 내부의 @Mock 또는 @Spy 어노테이션이 붙은 필드 중에서 주입 대상 클래스와 동일한 타입의 필드를 찾아서 주입한다.
서비스 코드 작성
- MyList 클래스와 그에 의존하는 MyService 클래스가 있다고 가정한다.
@Component
public class MyList {
private List<String> list = new ArrayList<>();
public void add(String item) {
list.add(item);
}
public int size() {
return list.size();
}
}
@Service
public class MyService {
@Autowired
private MyList myList;
public void addItem(String item) {
myList.add(item);
}
public int getItemCount() {
return myList.size();
}
}
- 여기서 MyService의 메소드들이 MyList의 메소드들을 어떻게 호출하는지 테스트하려고 한다.
- 그러나 MyList의 실제 동작을 테스트하려는 것이 아니라, MyService가 MyList를 어떻게 사용하는지만 테스트하려는 것이다. 이럴 때 @Mock과 @InjectMocks를 사용할 수 있다.
서비스 테스트 코드 작성
@SpringBootTest
public class MyServiceTest {
@Mock
private MyList mockMyList;
@InjectMocks
@Autowired
private MyService myService;
@Test
public void whenUseInjectMocksAnnotation_thenCorrect() {
myService.addItem("one");
Mockito.verify(mockMyList).add("one");
Mockito.when(mockMyList.size()).thenReturn(100);
assertEquals(100, myService.getItemCount());
}
}
- 위의 테스트 코드에서, @Mock 어노테이션을 사용하여 MyList의 인스턴스 mockMyList를 Mock 객체로 생성했다. 그리고 @InjectMocks 어노테이션을 사용하여 MyService의 인스턴스 myService를 생성하고 mockMyList를 주입했다. (@InjectMocks가 자동으로 @Mock 객체를 필드에서 스캔한 후 찾아서 주입해준다.)
- 이제 myService.addItem("one")을 호출하면 내부적으로 mockMyList.add("one")이 호출된다. 그리고 myService.getItemCount()를 호출하면 내부적으로 mockMyList.size()가 호출된다. 따라서 mockMyList의 동작을 변경하면 myService의 동작도 변경된다.
- 테스트 클래스 내부에 @Mock 어노테이션이 붙은 MyList 타입의 필드가 있고, @InjectMocks 어노테이션이 붙은 MyService 타입의 필드가 있다면, Mockito는 MyService 클래스가 MyList 타입의 필드를 가지고 있는지 확인한다.
- 만약 MyService 클래스가 MyList 타입의 필드를 가지고 있다면, Mockito는 @Mock 어노테이션이 붙은 MyList 타입의 필드를 MyService 클래스의 해당 필드에 주입한다.
- 테스트 클래스 내부에 @Mock 어노테이션이 붙은 MyList 타입의 필드가 있고, @InjectMocks 어노테이션이 붙은 MyService 타입의 필드가 있다면, Mockito는 MyService 클래스가 MyList 타입의 필드를 가지고 있는지 확인한다.
- 이렇게 @InjectMocks를 사용하면, 테스트 대상 클래스가 의존하는 다른 클래스를 Mock 객체로 대체하여 테스트 대상 클래스만을 고립시킨 상태에서 테스트를 수행할 수 있다. 이는 테스트의 복잡성을 줄이고, 테스트의 목적에 집중할 수 있도록 도와준다.
반응형
'Spring > 테스트 코드' 카테고리의 다른 글
Spring 서비스 테스트 중 발견한 NPE 해결기 (0) | 2023.08.17 |
---|---|
[스프링, 스프링 부트] Spring test - service 테스트에서 만난 오류 (0) | 2023.08.16 |
[스프링, 스프링부트] Spring test - 테스트 코드의 기초(5) [CRUD 테스트] (0) | 2023.08.09 |
[스프링, 스프링 부트] Spring test - 테스트 코드의 기초(4) [mock 테스트] (0) | 2023.08.09 |
[스프링, 스프링 부트] Spring test - 테스트 코드의 기초(3) [Mockito.when() 메서드] (0) | 2023.08.09 |