테스트 코드를 자다보면 @SpringBootTest, @Runwith, @Mock, @MockBean, @Spy, @InjectMock 등의 다양한 어노테이션을 볼 수 있다.
JUnit4, Junit5에 따라서 조금 사용방법이 다른점이 있고,
SpringBoot버젼에 따라서도 조금 다른점이 있다. 정리를 해보고자 함.
주요역할
options
주요역할
JUnit5에서는 @ExtendWith(SpringExtension.class) 으로 변경되었으며, @SpringBootTest 내부에 내장되어 있기 때문에 명시적으로 작성하지 않아도 좋다. (어차피 대부분의 Junit4에서의 테스트의 경우 둘다 정의하기 때문에 내장시킨 것 같다)
아래 나오는 Example 은 Junit5를 기준으로 작성되었다.
주요역할
@Controller, @RestController, @ControllerAdvice, @JsonComponent,
Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, HandlerMethodArgumentResolver
업무하면서 한번도 써본적이 없는 것 같은데,
아마도 써야한다면 RequestParams, PathVariable의 검증이나 에러케이스의 응답처리 확인용으로 쓴다면 쓸 것 같긴 한데...
애초에 컨트롤러 보다는 서비스 단위로 단위테스트 돌리는 케이스가 많은듯!?
Example
// Junit5 버젼이기 때문에 @RunWith필요 없다. @WebMvcTest에도 내장
@WebMvcTest(ProductController.class)
public class ProductControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private ProductService productService;
@DisplayName("상품조회 테스트")
@Test
public void getProduct() throws Exception {
final List<String> list = List.of("갤럭시");
when(productService.getProduct()).thenReturn(list);
mvc.perform(get(("/product/all")))
.andExpect(status().isOk())
.andExpect(content().json("[\"갤럭시\"]"))
.andDo(print());
}
@DisplayName("상품등록 테스트")
@Test
public void insertProduct() throws Exception {
mvc.perform(post(("/product/insert"))
.param("productName", "아이패드"))
.andExpect(status().isOk())
.andExpect(content().string("OK"))
.andDo(print());
}
}
주요역할
정리하면 간단히 컨트롤러만 테스트 하는 경우 @WebMvcTest
Service와 Repository도 같이 돌려보는 경우 @AutoCongifurationMockMvc + @SpringBootTest
Example
@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ProductService productService;
@Order(2)
@DisplayName("[GET] /product/all")
@Test
public void getProduct() throws Exception {
// then
mvc.perform(get(("/product/all")))
.andExpect(status().isOk())
.andExpect(content().json("[\"아이패드\"]"))
.andDo(print());
}
@Order(1)
@DisplayName("[POST] /product/insert")
@Test
public void insertProduct() throws Exception {
mvc.perform(post(("/product/insert"))
.param("productName", "아이패드"))
.andExpect(status().isOk())
.andExpect(content().string("OK"))
.andDo(print());
}
@DisplayName("[POST] /product/insert (파라메터 없는 경우 에러케이스)")
@Test
public void insertProduct2() throws Exception {
mvc.perform(post(("/product/insert")))
.andExpect(status().isBadRequest()) // 400 에러 검증
.andDo(print());
}
}
@MockBean과 @Mock모두 stub 가능한 Mock 객체를 생성하는 역할을 한다.
MockBean은 스프링 하위 패키지에 존재하여 스프링 전용 Mock이라고 보면 되는데,
간단히 말하면 MockBean으로 정의한 객체는 AppCtx구성시 BeanFactory에 실제 Bean대신 MockBean을 생성하여 로딩한다.
Mock의 경우는 AppCtx, BeanFactory에 포함되지 않고 테스트 코드상에서 주입하거나 @InjectMock등의 추가 어노테이션으로 의존성 관리를 직접 해야한다.
@Mock
@SpringBootTest
class ProductServiceMockTest {
@InjectMocks
private ProductService productService;
@Mock
private ProductRepository productRepository;
@Test
void getProduct() {
// given (stub 처리, 테스트를 위한 데이터 준비)
final List<String> mockResult = Lists.newArrayList();
mockResult.add("갤럭시");
when(productRepository.getProduct()).thenReturn(mockResult);
//when (테스트 수행)
final List<String> results = productService.getProduct();
// then (검증)
assertThat(results).isNotNull();
assertThat(results).isNotEmpty();
assertThat(results).hasSize(1);
assertEquals(mockResult, results);
}
@Test
void insertProduct() {
// given (stub 처리, 테스트를 위한 데이터 준비)
final String product = "헬로우";
//when (테스트 수행)
productService.insertProduct(product);
// then (검증)
verify(productRepository, times(1)).insertProduct(product);
}
}
@MockBean
일반적으로 대부분의 단위 테스트가 이런 모습이 될 것 같다.
참고) ProductRepository가 Mock이 아니라면 그냥 지워버리면 Repository가 그대로 동작
@SpringBootTest
class ProductServiceMockTest {
@Autowired
private ProductService productService;
@MockBean
private ProductRepository productRepository;
@Test
void getProduct() {
// given (stub 처리, 테스트를 위한 데이터 준비)
final List<String> mockResult = Lists.newArrayList();
mockResult.add("갤럭시");
when(productRepository.getProduct()).thenReturn(mockResult);
//when (테스트 수행)
final List<String> results = productService.getProduct();
// then (검증)
assertThat(results).isNotNull();
assertThat(results).isNotEmpty();
assertThat(results).hasSize(1);
assertEquals(mockResult, results);
}
@Test
void insertProduct() {
// given (stub 처리, 테스트를 위한 데이터 준비)
final String product = "헬로우";
//when (테스트 수행)
productService.insertProduct(product);
// then (검증)
verify(productRepository, times(1)).insertProduct(product);
}
}
@Spy와 @SpyBean의 차이는 @Mock과 @MockBean의 차이와 같다. AppCtx, BeanFactory에 로딩되어 자동으로 주입되느냐, 명시적으로 @InjectMock을 넣어주어야 하느냐 차이.
@Spy
@SpyBean
Junit4 vs Junit5 변경사항
JUnit4 | Junit5 | 역할 |
---|---|---|
@Before/@After | @BeforeEach/@AfterEach | 각 테스트 호출 전/후에 수행 |
@BeforeClass/@AfterClass | @BeforeAll/@AfterAll | 전체 테스트 시작 전/후에 한 번만 수행 |
@Ignore | @Disabled | 대상 테스트를 무시하고 수행하지 않는다 |
@RunWith | @ExtendWith | ✨Junit테스트 러너를 결정한다. (@SpringBootTest / @WebMvcTest에 내장) |
@Test(expected = Exception) | @Test | expected 가 5에서 사라져서 예외 검증을 별도로 코드에서 해야함 |
5버젼에서는 일부 어노테이션이 변경된 점이 가장 눈에 띄고,
SpringBootTest가 Runwith를 내장하면서 생략해도 되는점이 좋은 것 같다.
다만, @Test(expected = Exception)옵션이 제외되고 @Test메소드에서 assertion 해야되는 부분은 좀 귀찮아진 것 같다.