저는 Spring 테스트 자료를 찾아보면서 같은 계층의 테스트라도 다른 어노테이션을 사용하거나
아니면 여러가지 어노테이션을 조합해서 사용 하는 것을 보면서 왜?라는 의문이 들었습니다.
어노테이션에 대해 정확히 알고 쓰기 위해 이 글을 작성하게 되었습니다.
그렇지만 어노테이션에 대한 상세한 설명 페이지를 원했는데 그런 친절한 페이지는 보이지 않았습니다.
레퍼런스, 스텍오버플로우, 블로그등을 참고하여 정리 하였습니다.
스프링은 단위 또는 통합하여 테스트 할 수 있습니다.
단위 테스트는 애플리케이션 코드가 외부 종속성을 올바르게 작동하는지 확인하지 않습니다.
단일 구성 요소에 초첨을 맞추기 때문에 외부 종속성을 모킹 합니다.
통합 테스트는 전체 애플리케이션을 범위에 포함하거나 특성 구성 요소만 포함 합니다.
Spring은 들어오는 HTTP 요청을 처리하고 컨트롤러에 전달합니다. 이렇게하면 거의 전체 스택이 사용되며 실제 HTTP 요청을 처리하는 것과 똑같은 방식으로 코드가 호출되지만 서버 시작 비용은 없습니다
Controller를 테스트하기에 앞서 MockMvc를 미리 코드 상단에 복붙하겠습니다.
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
이 테스트에서는 전체 Spring 애플리케이션 컨텍스트가 시작되지만 서버는 시작하지 않습니다.
아래와 같은 종속성을 갖고 있는 Controller가 있다고 가정 합니다.
이 서비스 레이어는 통합테스트에서도 그대로 사용 한다고 가정 합니다.
@Service
public class TestServiceImpl {
public String greet() {
return "Hello, World";
}
}
Controller 부분을 제외하고는 모두 Mock 객체를 활용 해야합니다.
그렇기 때문에 MockBean를 통해 서비스를 종속성을 주입 할 수 있습니다.
@WebMvcTest(TestController.class)
@AutoConfigureMockMvc
public class TestRestController {
@Autowired
private MockMvc mockMvc;
@MockBean
TestServiceImpl testService;
@Test
public void hello() throws Exception {
when(testService.greet()).thenReturn("Hello, Mock");
mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
}
}
testService는 Mock객체 이므로 호출 되었을 때 객체의 동작을 정의 해줘야 합니다.
위에서는 서비스의 greet를 호출하면 hello, Mock이라는 스트링을 반환하는 동작을 만들었습니다.
그리고 그것을 Mocito에서는 stub이라고 합니다.
stub까지 준비 되었다면 MockMvc를 통해 Spring Mvc를 테스트하면 됩니다.
주의 해야 할 점은 Mockmvc의 행위를 수행하기 전에 stub까지 완료 되어야 한다는 것 입니다.
즉 함수의 호출 순서에 유의하여 테스트 코드를 작성해야 한다는 것 입니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class TestRestController {
@Autowired
private MockMvc mockMvc;
@Autowired
TestServiceImpl testService;
@Test
public void hello() throws Exception {
mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
}
}
통합테스트에서는 when으로 시작하는 Mockito가 보이지 않습니다. 왜냐하면 SpringBootTest를 사용해서 모든 빈을 applicationcontext에 등록하고 그것을 활용하기 때문입니다.
즉 내가 만든 하위 레이어를 사용해 테스트 하기 때문에 테스트코드가 단순하며 별로의 추가 사항은 없습니다.
Controller의 테스트는 서버를 시작하는 비용이 없습니다.
1. Slice테스트를 통해 Controller부분의 로직만 검증
2. Integration테스트를 통해 하위 레이어를 포함하여 검증
너무 많은 Mocking이 필요한 경우 SpringBootTest를 사용 할수도 있고
Controller부분만 검증하면 되서 Slice테스트 한다고 합니다.
두가지 테스트 방법에 정답은 없다고 하니 상황에 맞추어 선택하면 될 것이라고 생각합니다.
웹 계층만 Bean으로 등록되기 때문에 Service, Repository 계층의 의존성은 Mock객체를 활용해야합니다. 그렇기 때문에 복잡한 로직이 컨트롤러에 있다면 통합 테스트를 고려 하는것도 좋을것 같습니다.
JUnit, Mockito 사용하여 Spring서비스 계층을 단위 테스트를 작성합니다.
아래의 코드는 userRepository에 종속성을 갖는 Service를 테스트 하기위해 작성 되었습니다.
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserServiceImpl userService;
public void create() throws Exception {
//given
User user = new User("seouler");
//when
when(userRepository.save(any(User.class))).willReturn(Optional.of(user));
User saveUser = userService.create(user);
//then
Assertions.assertEquals(user.getName(), saveUser.getName());
}
}
하위 Repository를 사용하지 않기 때문에 @Mock 어노테이션을 활용해서 목 객체를 만듭니다.
그리고 servicer의 메서드 로직 중 Repository를 사용하는 부분을 Stub해주시면 됩니다.
공식 문서
Extendwith(MocitoExtension.class) 설명
junit5 extendwith
참고 자료
Mockito 기본 사용법
EntityManagerJPA 테스트에서 사용 하기 위한 대안 입니다.
@DataJpaTest
class ExampleRepositoryTests {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void testExample() throws Exception {
this.entityManager.persist(new User("sboot", "1234"));
User user = this.repository.findByUsername("sboot");
assertThat(user.getUsername()).isEqualTo("sboot");
assertThat(user.getVin()).isEqualTo("1234");
}
}
repository의 메서드를 테스트하기에 앞서 persist를 한다든지 entityManager의 역활을 수행할 수 이 있습니다.
읽어 봐야 할 문서들
스프링부트 Test 어노테이션들
@WebMvcTest 정의 보시면 @AutoConfigureMockMvc까지 선언되어있습니다.