<aside> 1️⃣ Mock이 뭘까?
</aside>
“Mock"
**이라는 단어는 테스트에서 많이 사용되는 용어로, 실제 객체를 모방한 가짜 객체를 의미한다. 이러한 Mock 객체는 테스트를 수행하는 동안 실제 객체의 행동을 흉내내거나 예측된 방식으로 반응하도록 설정할 수 있다. 이를 통해 테스트를 보다 통제 가능한 환경에서 실행할 수 있다.
Spring MVC
**의 동작을 **모방(Mock)
**하는 라이브러리다. 이것을 사용하면 HTTP 요청을 **DispatcherServlet
**에 보내고 그 결과를 검증할 수 있다. 실제 네트워크를 통하지 않고도 Spring MVC의 동작을 테스트할 수 있어, 테스트 속도가 빠르고 어떤 **Controller
**와 **View
**가 호출되었는지, 어떤 상태 코드와 헤더가 반환되었는지 등을 확인할 수 있다.Mockito
**라는 테스팅 프레임워크에서 제공하는 어노테이션으로, 테스트 대상 클래스에 Mock
객체를 주입할 때 사용한다. 이 어노테이션을 사용하면 테스트 대상 클래스의 인스턴스를 생성하고, 그 안에 필요한 의존성을 Mock
객체로 자동 주입해준다.Mocking
**은 테스트를 보다 통제 가능하고 예측 가능한 환경에서 실행하기 위한 기법이다. MockMVC
, @InjectMocks
, MockConfiguration
등은 이러한 **Mocking
**을 쉽게 구현할 수 있도록 도와주는 도구들이다.<aside> 2️⃣ 왜 테스트에서 Mock 객체를 사용하는걸까?
</aside>
Mock
객체를 사용하는 주요한 이유는 테스트를 보다 효율적이고, 견고하게 만들기 위함이다.Mock
객체는 메모리 내에서 동작하기 때문에 테스트의 속도를 크게 향상시킬 수 있다.Mock
객체는 항상 동일한 동작을 보장하기 때문에 테스트의 견고성을 향상시킬 수 있다.Mock
객체는 간단한 설정만으로 원하는 동작을 설정할 수 있다.Mock
객체를 사용하면 테스트 대상 코드와 그 외의 코드를 명확하게 분리할 수 있다. 이는 테스트의 목적을 명확하게 하고, 테스트 코드를 이해하기 쉽게 만든다.Mock
객체는 테스트의 효율성, 견고성, 단순성, 명확성을 향상시키는 데 도움이 된다.<aside> 3️⃣ 주요 어노테이션
</aside>
@SpringBootTest:
@SpringBootTest
**는 Spring Boot 기반의 통합 테스트를 지원하는 어노테이션이다. 이 어노테이션을 사용하면 **Spring Boot
**의 모든 기능을 자동 설정으로 로드하여 테스트 환경을 구성한다. 즉, 실제 애플리케이션과 동일한 컨텍스트를 로드하여 테스트를 수행할 수 있다. 이는 컨트롤러부터 레포지토리까지 모든 빈을 로드하여 실제 환경과 가장 유사한 테스트 환경을 제공한다. 그러나 이로 인해 테스트 실행 시간이 길어질 수 있다.@SpringBootTest
public class MyServiceIntegrationTest {
@Autowired
private MyService myService;
@Test
public void testMyService() {
// myService를 이용한 테스트 코드 작성
}
}
@AutoConfigureMockMvc:
@AutoConfigureMockMvc
**는 Spring Boot 테스트에서 제공하는 어노테이션으로, 이 어노테이션을 사용하면 MockMvc
인스턴스를 자동으로 생성하고 주입해준다.@AutoConfigureMockMvc
어노테이션을 사용하면, 테스트 클래스 내에서 **@Autowired
**를 사용하여 MockMvc
인스턴스를 주입받을 수 있다. 이렇게 주입받은 MockMvc
인스턴스를 사용하여 HTTP 요청을 보내고 그 결과를 검증하는 테스트를 작성할 수 있다.@MockBean
@MockBean
어노테이션은 Spring Boot 테스트에서 제공하는 기능으로, Spring 컨텍스트에서 해당 빈을 Mock 객체로 교체한다. 이를 통해 Spring 애플리케이션의 일부분만을 테스트하면서, 특정 빈의 동작을 Mocking할 수 있다. 예를 들어, 외부 시스템과의 통신을 담당하는 빈을 Mocking하여 외부 시스템과의 실제 통신 없이 테스트를 수행할 수 있다.
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyServiceTest {
@MockBean
private MyRepository myRepository;
@Autowired
private MyService myService;
@Test
public void whenUseMockBeanAnnotation_thenMockBeanIsInjected() {
Mockito.when(myRepository.count()).thenReturn(123L);
long count = myService.count();
assertEquals(123L, count);
}
}
@MockBean
어노테이션을 사용하여 **MyRepository
**의 인스턴스 **myRepository
**를 Mock 객체로 생성했다. **myService.count()
**를 호출하면 내부적으로 **myRepository.count()
**가 호출되는데, 이를 **Mockito.when(myRepository.count()).thenReturn(123L);
**을 통해 123L을 반환하도록 설정했다. 따라서 **assertEquals(123L, count);
**는 통과한다.1,2,3 통합적용한 controller 테스트 코드
@Service
public class MyService {
// 실제로는 데이터베이스나 다른 외부 자원과 상호작용하는 코드가 있을 수 있습니다.
// 이 예시에서는 단순히 123L을 반환하도록 합니다.
public long countItems() {
return 123L;
}
}
@RestController
public class MyController {
@Autowired
private MyService myService;
@GetMapping("/items/count")
public String getItemsCount() {
long count = myService.countItems();
return String.valueOf(count);
}
}
@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 객체와 함께 테스트할 수 있다. 이는 테스트의 복잡성을 줄이고, 테스트의 목적에 집중할 수 있도록 도와준다.@Mock
@Mock
어노테이션은 Mockito 프레임워크에서 제공하는 기능으로, 해당 필드를 Mock 객체로 만든다. Mock 객체는 원래 객체와 동일한 타입이지만, 그 행동은 테스트 케이스에서 정의한 대로 동작한다. 이를 통해 특정 메소드가 호출되었을 때 어떤 값을 반환할지, 어떤 예외를 던질지 등을 설정할 수 있다.@Service
public class MyService {
@Autowired
private MyRepository myRepository;
public long countItems() {
return myRepository.count();
}
}
MyService
**의 countItems
메소드가 **MyRepository
**의 count
메소드를 어떻게 호출하는지 테스트하려고 한다. 그러나 **MyRepository
**의 실제 동작을 테스트하려는 것이 아니라, **MyService
**가 **MyRepository
**를 어떻게 사용하는지만 테스트하려는 것이다. 이럴 때 **@Mock
**을 사용할 수 있다.@Repository
public interface MyRepository extends JpaRepository<MyEntity, Long> {
}
@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 객체로 대체하여 테스트 대상 클래스만을 고립시킨 상태에서 테스트를 수행할 수 있다. 이는 테스트의 복잡성을 줄이고, 테스트의 목적에 집중할 수 있도록 도와준다.@InjectMocks
@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
**를 주입했다.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
클래스의 해당 필드에 주입한다.@InjectMocks
**를 사용하면, 테스트 대상 클래스가 의존하는 다른 클래스를 Mock 객체로 대체하여 테스트 대상 클래스만을 고립시킨 상태에서 테스트를 수행할 수 있다. 이는 테스트의 복잡성을 줄이고, 테스트의 목적에 집중할 수 있도록 도와준다.@Spy
@Spy
어노테이션은 Mockito에서 제공하는 기능으로, 실제 객체를 사용하면서 일부 메소드의 동작만 변경하고 싶을 때 사용합니다. Spy 객체는 원래 객체와 모든 행동이 동일하지만, 원하는 메소드만 Mocking하여 특정 동작을 변경할 수 있습니다. 이를 통해 특정 메소드의 동작만을 테스트하거나, 특정 메소드의 동작을 재정의할 수 있습니다.@Spy
private List<String> spyList = new ArrayList<>();
@Test
public void whenUseSpyAnnotation_thenSpyIsInjectedCorrectly() {
spyList.add("one");
spyList.add("two");
Mockito.verify(spyList).add("one");
Mockito.verify(spyList).add("two");
assertEquals(2, spyList.size());
Mockito.doReturn(100).when(spyList).size();
assertEquals(100, spyList.size());
}
위의 코드에서, @Spy
어노테이션을 사용하여 **ArrayList<String>
**의 인스턴스 **spyList
**를 Spy 객체로 생성했습니다. **spyList.add("one")
**과 **spyList.add("two")
**를 호출하면 실제로 아이템이 추가되고 리스트의 크기는 2가 됩니다. 그런 다음 **Mockito.doReturn(100).when(spyList).size();
**을 통해 size()
메소드가 호출되면 100을 반환하도록 설정했습니다. 따라서 **assertEquals(100, spyList.size());
**는 통과합니다.
@Captor
@Captor
어노테이션은 Mockito에서 제공하는 기능으로, Mock 객체의 메소드 호출 시 전달되는 인자를 캡처할 때 사용합니다. 이를 통해 Mock 객체에 어떤 값이 전달되었는지 검증할 수 있습니다. 예를 들어, 메소드가 특정 값으로 호출되었는지, 특정 순서로 호출되었는지 등을 검증할 수 있습니다@Mock
private List<String> mockList;
@Captor
private ArgumentCaptor<String> argCaptor;
@Test
public void whenUseCaptorAnnotation_thenTheSam() {
mockList.add("one");
Mockito.verify(mockList).add(argCaptor.capture());
assertEquals("one", argCaptor.getValue());
}