Integration Test (통합 테스트)
├── Module Integration Test (모듈 통합 테스트)
├── Slice Test (슬라이스 테스트)
└── End-to-End (e2e) Test (종단 간 테스트)
2개 이상의 모듈이 결합된 테스트를 의미합니다.
Spring의 도움을 받지 않기 위해서는 생성자 주입, setter 주입의 경우에만 가능합니다.
필드 주입의 경우 해당 필드가 private로 선언되기 때문에 spring의 도움 없이는 초기화 할 방법이 없기 때문입니다.
단위 테스트와 마찬가지로 작성해주면 됩니다.
@ExtendWith(MockitoExtension.class) // MockitoExtension으로 Mockito 어노테이션을 자동 처리
public class ServiceATest {
@Mock
private ServiceB serviceB; // Mockito가 자동으로 모의 객체를 생성
@InjectMocks
private ServiceA serviceA; // Mockito가 자동으로 모의 객체를 주입
@Test
public void testProcess() {
// Given
when(serviceB.perform()).thenReturn("Mocked ServiceB");
// When
String result = serviceA.process();
// Then
assertEquals("ServiceA processing: Mocked ServiceB", result);
}
}
@SpringBootTest의 도움을 받아 진행할 수 있습니다.
Test Class에 @SpringbootTest를 추가 한 후 실제 객체를 주입할 객체에는 @AutoWired를, mocking 할 객체에는 @MockBean을 적어주면 됩니다.
Service A -> Service B -> Service C의 형태로 의존성이 엮여 있을 때를 살펴보겠습니다.
@ExtendWith(MockitoExtension.class)
@SpringBootTest
class Test {
@AutoWired
Service A a;
@AutoWired
Service B b;
@MockBean
Service C c;
}
위와 같이 설정하는 경우 Service A와 B는 Spring에 의해 실제 객체가 주입되며 Service C는 mocking되어 주입됩니다.
이렇게 되었을 때 Service A나 Service B에서 호출하는 service C의 method 중 return값을 스터빙해주어야 정상적인 테스트가 진행될 수 있습니다.
@SpringBootTest를 사용하면 의존성을 application context로 부터 bean을 주입받게 됩니다.
테스트에서 의존성은 실제 객체, null, mock 객체 셋 중 하나가 주입된다고 했었습니다. 단위 테스트의 경우 mocking하지 않으면 null이 주입되지만
@SpringBootTest를 사용 시 mocking을 진행하지 않는다면 실제 객체가 주입됩니다.
이에 따라 초기화 과정에서 오랜 시간이 걸리며 좋은 테스트의 특징 중 하나인 '테스트는 빨라야한다'와 '테스트는 격리된 환경에서 실행되어야 한다'를 어기게 됩니다.
SpringBootTest에서 특정 클래스만 로드하는 방법을 살펴보겠습니다.
@ContextConfiguration@SpringBootTest
@ContextConfiguration(classes = {ServiceA.class, ServiceB.class}) // 필요한 클래스만 등록
public class MyServiceTest {
@Autowired
private ServiceA serviceA;
@Autowired
private ServiceB serviceB;
@MockBean
private Repository repository;
}
@ComponentScan.Filter@SpringBootTest
@ComponentScan(
basePackages = "com.example",
includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {ServiceA.class, ServiceB.class}),
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {MyRepository.class})
)
public class MyServiceTest {
@Autowired
private ServiceA myService;
@MockBean
private Repository repository; // 필요한 의존성 모킹
@Test
public void testMyService() {
// 테스트 로직
}
}
슬라이스 테스트는 특정 계층을 목표로 한 테스트입니다.
Spring은 이러한 slice test를 돕기 위한 헬퍼 어노테이션을 제공합니다.
Spring 테스트의 헬퍼 어노테이션을 사용하게 되면 의존성은 application context를 통해 주입받게 되므로
@MockBean을 사용해야 합니다.
@WebMvcTestWeb layer만을 테스트하기 위한 test로 Controller, controller advice, dispatcher servlet등 Spring mvc Container는 bean으로 등록되며 @Service, @Repository등은 null이 주입됩니다.
@WebMvcTest(UserController.class) // UserController만 로드
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void testGetUserById() throws Exception {
// given
User user = new User(1L, "John Doe", "john.doe@example.com");
given(userService.getUserById(1L)).willReturn(user);
// when, then
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("John Doe")))
.andExpect(jsonPath("$.email", is("john.doe@example.com")));
}
}
통상 controller 단위 테스트로 WebMvcTest를 소개하는 경우가 많습니다. 하지만 WebMvc의 경우 @RequestParam과 같은 json 직렬화 및 response json 역직렬화나 Spring Mvc의 dispatcherServlet, Controller advice등 구성요소가 로드되어 1개 이상의 모듈이 결합된 것이기 때문입니다.
@DataJpaTestJPA를 통한 데이터베이스 계층을 테스트하기 위한 어노테이션입니다.
DataJpaTest는 JpaRepository들을 bean으로 로드 합니다.
이 때 @Transactional이 자동으로 적용됩니다.
Spring Test Framework는 테스트 메소드 수행 후 자동적으로 rollback하도록 설정되어있습니다.
Rollback을 적용하고 싶지 않다면 @commit을 사용하면 됩니다.
JpaRepository를 extends받지 않은 경우 Bean으로 등록되지 않습니다.
이는 @Repository의 경우 일반적인 @Component로 간주되기 때문입니다.
아래 코드의 경우 의존성을 불러올 수 없습니다.
@Repository
class CustomRepository{
}
@DataJpaTest
class TestRepository {
@AutoWired
CustomRepository customRepository; // null로 주입
}
@JsonTestJson 직렬화, 역직렬화 테스트를 수행합니다. 보통 dto나 request, response 변환에 대해 테스트를 진행합니다.
@JsonTest
public class UserJsonTest {
@Autowired
private JacksonTester<User> json;
@Test
public void testSerialize() throws Exception {
User user = new User(1L, "John Doe", "john.doe@example.com");
// 객체를 JSON으로 직렬화
JsonContent<User> jsonContent = json.write(user);
// JSON 문자열이 예상대로 직렬화되었는지 검증
assertThat(jsonContent).hasJsonPathNumberValue("$.id");
assertThat(jsonContent).extractingJsonPathNumberValue("$.id").isEqualTo(1);
assertThat(jsonContent).hasJsonPathStringValue("$.name");
assertThat(jsonContent).extractingJsonPathStringValue("$.name").isEqualTo("John Doe");
assertThat(jsonContent).hasJsonPathStringValue("$.email");
assertThat(jsonContent).extractingJsonPathStringValue("$.email").isEqualTo("john.doe@example.com");
}
@Test
public void testDeserialize() throws Exception {
String jsonString = "{\"id\":1,\"name\":\"John Doe\",\"email\":\"john.doe@example.com\"}";
// JSON 문자열을 객체로 역직렬화
ObjectContent<User> userObjectContent = json.parse(jsonString);
// 객체의 필드가 예상대로 역직렬화되었는지 검증
assertThat(userObjectContent.getObject().getId()).isEqualTo(1);
assertThat(userObjectContent.getObject().getName()).isEqualTo("John Doe");
assertThat(userObjectContent.getObject().getEmail()).isEqualTo("john.doe@example.com");
}
}
E2E Test는 종단간 테스트를 의미하며 보통 백엔드에서는 api 테스트를 의미합니다.
E2E 테스트는 요청부터 응답까지 실제 환경에서 필요한 모든 모듈을 로드하여 진행합니다.
Rest api를 테스트 할 수 있는 라이브러리입니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyRestAssuredE2ETest {
@LocalServerPort
private int port;
@BeforeEach
public void setUp() {
RestAssured.port = port;
}
@Test
public void testGetUser() {
given()
.contentType(ContentType.JSON)
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.body("id", equalTo(1))
.body("name", equalTo("John Doe"))
.body("email", equalTo("john.doe@example.com"));
}
}