Spring과 테스트 (2) 통합 테스트

굴착드릴·2024년 8월 26일

개요

Integration Test (통합 테스트)
  ├── Module Integration Test (모듈 통합 테스트)
  ├── Slice Test (슬라이스 테스트)
  └── End-to-End (e2e) Test (종단 간 테스트)

통합테스트?

2개 이상의 모듈이 결합된 테스트를 의미합니다.

모듈 통합 테스트

Spring의 도움을 받지 않는 테스트 방법

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);
    }
}

Spring의 도움을 받는 테스트 방법

@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값을 스터빙해주어야 정상적인 테스트가 진행될 수 있습니다.

Application context에 특정 bean만 로드

@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을 사용해야 합니다.

@WebMvcTest

Web 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개 이상의 모듈이 결합된 것이기 때문입니다.

@DataJpaTest

JPA를 통한 데이터베이스 계층을 테스트하기 위한 어노테이션입니다.

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로 주입
}

@JsonTest

Json 직렬화, 역직렬화 테스트를 수행합니다. 보통 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 테스트

E2E Test는 종단간 테스트를 의미하며 보통 백엔드에서는 api 테스트를 의미합니다.

E2E 테스트는 요청부터 응답까지 실제 환경에서 필요한 모든 모듈을 로드하여 진행합니다.

RestAssured

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"));
    }
}
profile
두두두두..

0개의 댓글