[SpringBoot] 스프링부트 통합테스트, 단위테스트

2022. 11. 7. 17:42JAVA/Spring

통합테스트

  • @SpringBootTest

단위테스트

  • @JsonTest
  • @WebMvcTest
  • @WebFluxTest
  • @DataJpaTest
  • @RestClientTest

1. 통합 테스트

개요

  • 실제 운영 환경에서 사용될 클래스들을 통합하여 테스트
  • 단위 테스트와 같이 기능 검증을 위한 것이 아닌 스프링 프레임워크에서 전체적으로 플로우가 제대로 동작하는지 검증하기 위해 사용됨

장점

  • 애플리케이션의 설정과 모든 빈(Bean)을 모두 로드하기 때문에 운영환경과 가장 유사항 테스트가 가능함
  • 전체적인 플로우를 쉽게 테스트가 가능함

단점

  • 애플리케이션의 설정과 모든 빈(Bean)을 모두 불러오기 때문에 시간이 오래걸리고 무겁다
  • 테스트 단위가 크기 때문에 디버깅이 어려운 편

@SpringBootTest

  • 스프링부트는 @SpringBootTest 애노테이션을 통해 스프링부트 애플리케이션 테스트에 필요한 거의 모든 의존성을 제공함
  • @SpringBootTest는 통합 테스트를 제공하는 기본적인 스프링 부트 테스트 애노테이션
  • @SpringBootTest 애노테이션 사용시 Junit 버전에 따라 유의사항이 존재함
    • Junit4 : @RunWith(SpringRunner.class)와 함께 사용
    • Junit5 : 따로 명시할 필요 없음

 

@SpringBootTest의 옵션

1. properties

 

1.1 프로퍼티를 {key=value} 형식으로 직접 추가 가능함

@SpringBootTest(properties = {"name=yonghwan"})
public class SampleSpringBootTest {
    @Value("${name}")
    private String name;

    @Test
    public void testName(){
        assertThat(name).isEqualTo("yonghwan");
    }
}

1.2 프로퍼티의 키값으로 “spring.config.location”으로 설정하고 값으로 외부 파일을 설정하여 외부파일의 프로퍼티를 가져오는 것이 가능함

src/test/test.yml

yonghwan:
  name: YongHwan
  age: 20

@SpringBootTest 애노테이션의 properties 옵션을 이용하여 외부 파일을 다음과 같이 불러옴

@SpringBootTest(properties = {"spring.config.location = classpath:test.yml"})
public class SampleSpringBootTest {
    @Value("${yonghwan.age}")
    private int age;

    @Test
    public void testAge(){
        assertThat(age).isEqualTo(20);
    }
}

 

2. webEnvironment

  • 웹 테스트 환경 구성이 가능함
  • webEnvironment 파라미터를 이용하여 손쉽게 웹 테스트 환경을 선택할 수 있음
  • 웹 테스트 환경 종류
    • Mock
    • RANDOM_PORT
    • DEFINED_PORT
    • NONE

2.1 Mock

  • 실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어 제대로 구현하기 어려운 경우, 가짜 객체를 만들어 사용함
  • WebApplicationContext를 불러오며 내장된 서블릿 컨테이너가 아닌 Mock 서블릿을 제공함
  • @SpringBootTest 애노테이션의 webEnvironment 옵션의 기본값
  • @AutoConfigureMockMvc 애노테이션을 함께 사용하여 MockMvc를 사용한 테스트 진행 가능함.
    • MockMvc는 브라우저에서 요청과 응답을 의미하는 객체. Controller 테스트 사용을 용이하게 해주는 라이브러리
    • @AutoConfigureMockMvc 애노테이션은 Mock 테스트시 필요한 의존성을 제공함.
@Autowired
MockMvc mockMvc;

 

2.2 RANDOM_PORT

  • 내장된 WebApplicationContext를 불러오며 실제 서블릿 환경을 구성함
  • 임의의 포트를 지정함
  • 실제 서블릿 컨테이너를 사용하기 때문에 TestRestTemplate 사용

 

2.3 DEFINED_PORT

  • RANDOM_PORT와 동일하게 실제 서블릿 환경을 구성하지만, 포트는 애플리케이션 프로퍼티에서 지정한 포트를 지정합니다.
  • 실제 서블릿 컨테이너를 사용하기 때문에 TestRestTemplate 사용

 

2.4 NONE

  • 기본적인 ApplicationContext를 불러옴

 

TestRestTemplate는 무엇인가?

  • 통합 테스트에 적합한 RestTemplate의 대안
  • 4xx 및 5xx는 예외가 발생하지 않음. 대신 Response 엔티티 및 상태 코드를 통해 탐지될 수 있음
  • 기본 인증 헤더가 선택적으로 포함될 수 있음 Apache Http Client 4.3.2 이상이 사용 가능한 경우 권장 클라이언트로 사용되며 기본적으로 쿠키 및 리다이렉션을 무시하도록 구성됨
  • 주입 문제를 방지하기 위해 이 클래스는 의도적으로 RestTemplate를 확장하지 않음
    • 기본 RestTemplate에 접근 권한이 필요한 경우 getRestTemplate()를 사용함
@Autowired
TestRestTemplate testRestTemplate;

 

@MockBean

  • Mock 객체를 빈(Bean)으로써 등록할 수 있음
  • @MockBean 애노테이션 적용시 스프링의 ApplicationContext는 Mock 객체를 빈으로 등록하며, @MockBean으로 선언된 객체와 같은 이름과 타입으로 이미 빈이 등록되어 있다면 해당 빈은 선언한 @MockBean으로 대체된다.
@MockBean
SampleService sampleService;

 

실습준비

1. 컨트롤러 정의

@RestController
public class SampleController {
    @Autowired
    private SampleService sampleService;

    @GetMapping("/hello")
    public String hello(){
        return "hello " + sampleService.getName();
    }
}

2. 서비스 정의

@Service
public class SampleService {
    public String getName() {
        return "yonghwan";
    }
}

 

실습, 스프링 부트 테스트 웹 환경 : Mock

1. 테스트 코드 추가

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class SampleControllerMockTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    public void testHello() throws Exception {
        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello yonghwan"))
                .andDo(print());

    }
}
  • webEnvironment = SpringBootTest.WebEnvironment.MOCK : 스프링부트 테스트 웹 환경을 MOCK으로 설정되어 서블릿 컨테이너가 실행 안됨
  • @AutoConfigureMockMvc : MockMvc 객체가 실행하는데 필요한 의존성을 제공함
  • MockMvc mockMvc : MockMvc 빈을 자동주입함

 

2. 실행결과를 확인합니다.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = kr.yh.spring_test.SampleController
           Method = kr.yh.spring_test.SampleController#hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"14"]
     Content type = text/plain;charset=UTF-8
             Body = hello yonghwan
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
  • 위 실행결과들의 확인은 mockMvc 객체의 호출로 전부 확인이 가능합니다.

 

실습, 스프링 부트 테스트 웹 환경 : RANDOM_PORT

 

1. TestRestTemplate 객체를 통한 컨트롤러 테스트

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleControllerRandomPortTest {
    @Autowired
    TestRestTemplate testRestTemplate;

    @Test
    public void testHello() throws Exception {
        String result = testRestTemplate.getForObject("/hello", String.class);
        assertThat(result).isEqualTo("hello yonghwan");
    }
}
  • TestRestTemplate 객체만을 사용하여 컨트롤러 테스트를 수행하게 되면 컨트롤러 뿐만 아니라 서비스 레이어까지 올라가 테스트하기 때문에 테스트 비용이 상승하게 됩니다.
  • 위 문제에 대한 해결안으로 @MockBean 애노테이션을 사용하여 사용된 서비스를 모킹(Mocking)하여 서비스 비용을 낮춥니다.

 

2. TestRestTemplate & @MockBean 애노테이션을 사용하여 컨트롤러 테스트

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleControllerRandomPortTest {
    @Autowired
    TestRestTemplate testRestTemplate;

    @MockBean
    SampleService mockSampleService;

    @Test
    public void testHelloUsingMockBean(){
        when(mockSampleService.getName()).thenReturn("yonghwan");

        String result = testRestTemplate.getForObject("/hello", String.class);
        assertThat(result).isEqualTo("hello yonghwan");
    }

}
  • SampleService 타입은 이미 빈으로 등록되어 있기 때문에 SampleService 빈은 목빈(MockBean)으로 대체됩니다.
  • when(mockSampleService.getName()).thenReturn(”yonghwan”) : SampleService.getName() 메서드가 호출될때 특정한 값을 반환함으로써 서비스 객체를 호출하지 않게합니다.

 

3. WebTestClient & @MockBean 애노테이션을 사용하여 컨트롤러 테스트

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleControllerRandomPortTest {
    @MockBean
    SampleService mockSampleService;

    @Autowired
    WebTestClient webTestClient;

    @Test
    public void testHello_webTestClient(){
        when(mockSampleService.getName()).thenReturn("yonghwan");

        webTestClient.get().uri("/hello")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("hello yonghwan");
    }
}
  • WebTestClient
    • WebClient를 감싸고 있는 얇은 층으로, WebClient를 이용하여 요청을 수행하며 응답을 검증하기 위한 효과적인 API를 제공함.
    • WebTestClient는 Mock Request 및 Response를 사용하여 WebFlux 애플리케이션에 바인딩하며, HTTP 우베 서버를 테스트할 수 있음
    • spring-boot-start-webflux 의존성 필요
  • WebTestClient와 RestTemplate 비교
    • WebTestClient는 spring5부터 지원, RestTemplate는 spring 3부터 지원
    • WebTestClient는 NonBlock + Async Web Client, RestTemplate는 Sync Client
    • 편리함 : WebTestClient > MockMvc > TestRestTemplate

 

2. 단위 테스트

단위 테스트의 종류

  • @JsonTest
  • @WebMvcTest
  • @WebFluxTest
  • @DataJpaTest
  • @RestClientTest

 

@JsonTest 애노테이션

  • @JsonTest 애노테이션을 사용하면 JSON 직렬화를 테스트하는데 필요한 스프링 빈만을 사용하여 Spring TestContext를 자동으로 구성할 수 있음
  • 이 Spring TestContext는 웹 레이어 도는 영속성 레이어와 관련되지 않음
  • JacksonTester, JsonTester, GsonTester를 지원함

실습, @JsonTest를 이용한 직렬화, 역직렬화 테스트

1. 직렬화/역직렬화 대상이 되는 클래스 정의

@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy.class)
@Getter
@Setter
@AllArgsConstructor
public class UserDetails {
    private Long id;
    private String firstName;
    private String lastName;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd")
    private LocalDate dateOfBirth;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private boolean enabled;
}
  1. @JsonTest 애노테이션을 사용하여 직렬화, 역직렬화 관련된 테스트 코드 추가
@JsonTest
public class SampleControllerJsonTest {
    @Autowired
    private JacksonTester<UserDetails> json;

    @Test
    public void testSerialize() throws IOException {
        UserDetails userDetails =
                new UserDetails(1L, "Duke", "Java",
                        LocalDate.of(1995, 1, 1), true);

        JsonContent<UserDetails> result = this.json.write(userDetails);
        assertThat(result).hasJsonPathStringValue("$.firstname");
        assertThat(result).extractingJsonPathStringValue("$.firstname").isEqualTo("Duke");
        assertThat(result).extractingJsonPathStringValue("$.lastname").isEqualTo("Java");
        assertThat(result).extractingJsonPathStringValue("$.dateofbirth").isEqualTo("1995.01.01");
        assertThat(result).doesNotHaveJsonPath("$.enabled");
    }

    @Test
    public void testDeserialize() throws IOException {

        String jsonContent = "{\\"firstname\\":\\"Mike\\", \\"lastname\\": \\"Meyer\\"," +
                " \\"dateofbirth\\":\\"1990.05.15\\"," +
                " \\"id\\": 42, \\"enabled\\": true}";

        UserDetails result = this.json.parse(jsonContent).getObject();

        assertThat(result.getFirstName()).isEqualTo("Mike");
        assertThat(result.getLastName()).isEqualTo("Meyer");
        assertThat(result.getDateOfBirth()).isEqualTo(LocalDate.of(1990, 05, 15));
        assertThat(result.getId()).isEqualTo(42L);
        assertThat(result.isEnabled()).isEqualTo(true);
    }

}

 

@WebMvcTest 애노테이션

  • 웹상에서 요청과 응답에 대한 테스트가 가능함
  • @WebMvcTest 애노테이션을 사용하면 @Controller, @ControllerAdvice, @JsonComponent, @JsonFilter, WebMvcConfigure, HandlerMethodArgumentResolver만 로드되기 때문에 전체 테스트보다 가벼움

 

실습, @WebMvcTest를 이용한 컨트롤러 테스트

@WebMvcTest(SampleController.class) // 컨트롤러만 빈으로 등록
public class SampleControllerWebMvcTest {
    @MockBean
    SampleService mockSampleService; // 필요한 빈은 MockBean으로 채워줌

    @Autowired
    MockMvc mockMvc;

    @Test
    public void testHello() throws Exception {
        when(mockSampleService.getName()).thenReturn("yonghwan");

        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello yonghwan"))
                .andDo(print());

    }
}

 

@WebFluxTest

  • Spring WebFlux 컨트롤러가 에상대로 작동하는지 테스트하려면 @WebFluxTest 애노테이션을 사용할 수 있음
    • Spring WebFlux : 스프링5에서 새로 등장한 웹 애플리케이션에서 리액티브 프로그래밍을 제공하는 프레임워크
  • @WebFluxTest 애노테이션은 Spring WebFlux 인프라를 자동으로 구성하고 스캔 빈을 다음과 같이 제한함
    • @Controller, @ControllerAvice, @JsonComponent, Converter, GenericConverter 및 WebFluxConfigurer
  • @WebFluxTest 애노테이션을 사용하면 일반 @Component 빈은 검색되지 않음
  • Jackson Moudel과 같은 추가 구성요소를 등록해야하는 경우 테스트에서 @Import를 사용하여 추가 구성 클래스를 가져올 수 있음
  • @WebFluxTest는 단일 컨트롤러로 제한할 수 있으며 @MockBean 애노테이션과 함께 사용하여 비용이 큰 객체를 목 객체로 대체할 수 있음
  • @WebFluxTest는 전체 HTTP 서버를 시작하지 않고도 WebFlux 컨트롤러를 신속하게 테스트할 수 있는 방법을 제공하는 WebTestClient를 자동구성함

 

실습, @WebFluxTest을 이용한 컨트롤러 테스트

1. SampleController를 테스트하기 위한 테스트 코드 추가

@WebFluxTest(SampleController.class)
public class SampleControllerWebFluxTest {
    @MockBean
    SampleService mockSampleService;

    @Autowired
    WebTestClient webTestClient;

    @Test
    public void testHello(){
        when(mockSampleService.getName()).thenReturn("yonghwan");

        webTestClient.get().uri("/hello")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("hello yonghwan");
    }
}

 

@DataJpaTest

  • JPA와 관련된 설정만 불러옴.
  • @Entity 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성함
  • 기본적으로 인메모리 데이터베이스를 이용함
  • 데이터소스의 설정이 정상적인지, JPA를 사용해서 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능함
  • @AutoConfigureTestDataBase : 데이터 소스를 어떤 걸로 사용할지에 대한 결정
    • Replace.Any : 기본적으로 내장된 데이터소스 사용
    • Replace.NONE : @ActiveProfiles 기준으로 프로파일이 설정됨
  • @DataJpaTest : 테스트가 끝날때마다 자동으로 테스트에 사용한 데이터를 롤백함

 

실습, @DataJpaTest를 이용한 데이터베이스 데이터 저장 테스트

1. 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
runtimeOnly 'com.h2database:h2'

 

2. 엔티티 클래스 정의

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Sample {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sample_id")
    private Long id;
    private String name;
    private int age;
}

 

3. Repository 인터페이스 정의

@Repository
public interface SampleRepository extends JpaRepository<Sample, Long> {
}

 

4. application-test.yml 파일에 데이터 소스 관련 설정

datasource:
  url : jdbc:h2:mem:test
  username : sa
  password :
  driver-class-name: org.h2.Driver

h2:
  console:
    enabled: true
    path: /h2-console

jpa:
  hibernate:
    ddl-auto: create
  properties:
    hibernate:
      format_sql: true

 

5. 테스트코드 추가

@DataJpaTest
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class SampleJpaTest {
    @Autowired
    private SampleRepository sampleRepository;
    
    @Test
    public void testSave(){
        Sample sample = new Sample(null, "yonghwan", 30);
        Sample savedSample = sampleRepository.save(sample);
        Assertions.assertThat(savedSample).isEqualTo(sample);
    }
}

 

@RestClientTest

  • Rest 통신의 JSON 형식이 예상대로 응답을 반환하는지 등을 테스트함
  • MockRestServiceServer : 클라이언트와 서버 사이의 REST 테스트를 위한 객체. 내부에서 RestTemplate을 바인딩하여 실제로 통신이 이루어지게끔 구성할 수 있음.

 

실습, @RestClientTest를 이용한 응답 결과 테스트

1. Sample 클래스를 정의

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Sample {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sample_id")
    private Long id;
    private String name;
    private int age;
}

 

2. 테스트 코드를 추가

@RestClientTest(SampleServiceClient.class)
public class SampleControllerRestClientTest {

    RestTemplate restTemplate;

    MockRestServiceServer server;

    @Autowired
    ObjectMapper objectMapper;

    @BeforeEach
    public void setup() throws JsonProcessingException {
        restTemplate = new RestTemplateBuilder().build();
        server = MockRestServiceServer.createServer(restTemplate);

        String sampleString = objectMapper.writeValueAsString(new Sample(null, "yonghwan", 20));

        server.expect(MockRestRequestMatchers.requestTo("/yonghwan/details"))
                .andRespond(MockRestResponseCreators.withSuccess(sampleString, MediaType.APPLICATION_JSON));
    }

    @Test
    public void test() {
        Sample sample = restTemplate.getForObject("/{name}/details", Sample.class, "yonghwan");

        assertThat(sample.getName()).isEqualTo("yonghwan");
        assertThat(sample.getAge()).isEqualTo(20);
    }
}
  • setup() 메서드에서 sampleString 문자열 변수에 JSON 형식으로 객체의 값들을 저장하고 서버에서 “/yonghwan/details” 요청이 들어오면 sampleString 문자열을 JSON 형식으로 응답하라고 가정합니다.
  • 테스트에서 RestTemplate를 이용하여 URL에 따른 응답을 받아 Sample 인스턴스 변수에 저장합니다.

 

References

soruce code : https://github.com/yonghwankim-dev/springboot_study/tree/main/springboot_utilization/src/test/java/kr/yh/spring_test
[spring] 스프링부트의 테스트

[스프링부트 (9)] SpringBoot Test(2) - @SpringBootTest로 통합테스트 하기

Spring Boot Test 종합 정리 ( 테스트종류, JUnit5 )

Testing JSON Serialization With @JsonTest and Spring Boot

46.3.11 자동 구성된 Spring WebFlux 테스트

Quick Guide to @RestClientTest in Spring Boot

[인프런] 스프링부트 개념과 활용