2023년 4월 16일 일요일

테스트 관련 메모

테스트에 대해 헷갈리는 내용이 많음. 테스트 환경을 잘 만드는 것도 중요할 듯 하다.

  • chatGPT

    Java Spring 환경에서 사용할 수 있는 대표적인 테스트 프레임워크는 다음과 같습니다.
    
    JUnit: Java 언어로 작성된 단위 테스트 프레임워크입니다. Spring에서도 가장 많이 사용되는 테스트 프레임워크 중 하나입니다.
    
    Mockito: Mock 객체를 생성하고 관리하는 라이브러리로, Spring에서 유닛 테스트를 작성할 때 자주 사용됩니다.
    
    Spring Test: Spring Framework에서 제공하는 테스트 프레임워크입니다. Spring 애플리케이션의 통합 테스트, 웹 애플리케이션의 테스트 등을 작성할 때 사용됩니다.
    
    TestNG: JUnit과 유사한 기능을 제공하는 테스트 프레임워크로, Spring에서도 사용 가능합니다. 테스트 그룹, 의존성 설정 등 다양한 기능을 제공합니다.
    
    Selenium: 웹 애플리케이션의 UI 테스트를 자동화하는 데 사용되는 프레임워크입니다. Spring에서도 웹 애플리케이션의 UI 테스트를 작성할 때 사용됩니다.
    
    Cucumber: BDD(Behavior Driven Development)를 지원하는 테스트 프레임워크입니다. 비즈니스 요구사항을 기반으로 시나리오를 작성하고, 이를 테스트 코드로 변환하는 방식으로 작성합니다.
    
    Arquillian: Java EE 애플리케이션의 통합 테스트를 위한 프레임워크입니다. Spring과 함께 사용할 수 있습니다.
    
    이외에도 다양한 테스트 프레임워크들이 있지만, 위에 소개한 프레임워크들은 Spring 환경에서 가장 많이 사용되는 대표적인 프레임워크들입니다.

용어 정리

  • 자세한 사용법은 패스. 모두 다양한 사용방법을 제공함

Junit

  • 유닛 테스트 프레임워크

  • 흔히 사용하는 @Test

  • 스프링 부트 2.2.x 이상 부터는 Junit5

  • Junit5 는 세 가지 모듈로 구성

    • Junit Platform
    • Junit Jupiter
    • Junit Vintage : Junit4 등 이전 버전의 테스트를 실행할 수 있도록 지원
    • import org.junit.jupiter.api.Assertions;
      import org.junit.jupiter.api.Test;

      class JunitTest {

      @Test
      void test1() {
      String a = "1";
      int number = 1;

      Assertions.assertEquals(Integer.parseInt(a), number);
      }
      }

Mockito

  • 단위테스트를 작성할 때, 모든 의존성을 직접 만들지 않고도 코드를 테스트할 수 있도록 도와줌

    • Mockito 를 이용하여 실제 객체를 모방한 가짜 객체 생성이 가능함
    • Stubbing 을 통해 mock 객체의 메소드를 실행했을 때 어떤 리턴 값을 리턴할 지 정의 가능
    • import org.junit.jupiter.api.Assertions;
      import org.junit.jupiter.api.Test;

      import static org.mockito.ArgumentMatchers.any;
      import static org.mockito.Mockito.*;

      class MockitoTest {
      /**
      * 예시를 위한 테스트
      * 이 테스트에서는 db 에 저장되는 로직이 아니라 다른 로직이 동작하는 지 확인하고 싶음
      * service 의 save 가 호출되면 count 가 증가된다. 이 count 만 정상 동작하는 지 확인하고 싶음
      */
      @Test
      void memberService_count_test() {
      Member member = new Member("name");
      MemberRepository memberRepository = mock(MemberRepository.class);
      when(memberRepository.save(any())).thenReturn(member);

      // mock repository 주입
      MemberService memberService = new MemberService(memberRepository);

      MemberDto dto = new MemberDto("name");
      MemberDto dto2 = new MemberDto("name2");

      Member saveMember = memberService.save(dto);
      Member saveMember2 = memberService.save(dto2);

      // dto2 에 name2 를 저장했지만, mock repository 에 의해 name 가 됨
      Assertions.assertEquals(saveMember.getName(), dto.getName());
      Assertions.assertEquals(saveMember2.getName(), dto.getName());

      // mock 객체에 대해서 발생한 동작에 대해서 확인 가능
      verify(memberRepository, times(2)).save(any());

      // save 를 호출하였을 때 count 가 증가되는 지 확인한다
      Assertions.assertEquals(memberService.getCount(), 2);
      }
      }
    • // 어노테이션으로 사용
      @ExtendWith(MockitoExtension.class)
      class MockitoTest2 {

      @Mock
      private MemberRepository memberRepository;

      @InjectMocks
      private MemberService memberService;

      /**
      * 예시를 위한 테스트
      * 이 테스트에서는 db 에 저장되는 로직이 아니라 다른 로직이 동작하는 지 확인하고 싶음
      * service 의 save 가 호출되면 count 가 증가된다. 이 count 만 정상 동작하는 지 확인하고 싶음
      */
      @Test
      void memberService_count_test() {
      Member member = new Member("name");
      when(memberRepository.save(any())).thenReturn(member);

      MemberDto dto = new MemberDto("name");
      MemberDto dto2 = new MemberDto("name2");

      Member saveMember = memberService.save(dto);
      Member saveMember2 = memberService.save(dto2);

      // dto2 에 name2 를 저장했지만, mock repository 에 의해 name 가 됨
      Assertions.assertEquals(saveMember.getName(), dto.getName());
      Assertions.assertEquals(saveMember2.getName(), dto.getName());

      // mock 객체에 대해서 발생한 동작에 대해서 확인 가능
      verify(memberRepository, times(2)).save(any());

      // save 를 호출하였을 때 count 가 증가되는 지 확인한다
      Assertions.assertEquals(memberService.getCount(), 2);
      }
      }

@SpringBootTest / @WebMvcTest

  • @SpringBootTest

    • 통합테스트 느낌
    • 프로젝트 내부에 있는 스프링 빈을 모두 등록하여 테스트에 필요한 의존성 추가
    • 실제로 구동되는 애플리케이션과 거의 동일한 환경을 제공
    • 단위 테스트가 아닌 Spring Framework 에서 전체적으로 Flow가 제대로 동작하는지 검증하기 위해 사용
    • 애플리케이션의 모든 계층(데이터베이스, 서비스, 컨트롤러 등)을 테스트할 수 있음
    • 시간이 오래걸림
    • // 많이 보이는 테스트로 그냥 reference 예제 사용
      @SpringBootTest
      @AutoConfigureMockMvc
      class MyMockMvcTests {
      // If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient
      @Test
      fun testWithWebTestClient(@Autowired webClient: WebTestClient) {
      webClient
      .get().uri("/")
      .exchange()
      .expectStatus().isOk
      .expectBody<String>().isEqualTo("Hello World")
      }

      }
  • @WebMvcTest

    • Controller 클래스를 테스트하기 위한 어노테이션
    • 애플리케이션의 다른 구성요소(service, repository) 를 모두 로드하기 않고도 특정 컨트롤러를 테스트할 수 있도록 함
    • 시간이 짧고, 불필요한 의존성을 로드하지 않음
    • // 많이 보이는 테스트로 그냥 reference 예제 사용
      @WebMvcTest(UserVehicleController.class)
      class MyControllerTests {

      @Autowired
      private MockMvc mvc;

      @MockBean
      private UserVehicleService userVehicleService;

      @Test
      void testExample() throws Exception {
      given(this.userVehicleService.getVehicleDetails("sboot"))
      .willReturn(new VehicleDetails("Honda", "Civic"));
      this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
      .andExpect(status().isOk())
      .andExpect(content().string("Honda Civic"));
      }

      }

WireMock

  • HTTP 기반의 웹 서비스를 mock 하는 도구
    • 특정한 HTTP 요청에 대한 응답으로 미리 지정해둔 형태의 HTTP 응답이 반환됨
  • chatGPT : WireMock은 언제 사용하면 좋을까?

    WireMock은 주로 다음과 같은 상황에서 사용됩니다.
    
    1. 외부 API 호출 테스트
    API를 사용하는 애플리케이션을 개발할 때, 외부 API 호출을 테스트하기 위해서는 외부 API 서버가 필요합니다. 그러나 외부 API 서버가 불안정하거나, 인증이 필요한 경우에는 테스트를 진행하기 어렵습니다. 이런 경우에는 WireMock을 사용하여 가짜 응답을 생성할 수 있습니다.
    
    2. 마이크로서비스 아키텍처에서의 서비스 통합 테스트
    마이크로서비스 아키텍처에서는 각각의 서비스가 독립적으로 개발되고, 운영됩니다. 이때 서비스 간의 통합 테스트를 진행해야 합니다. WireMock은 마이크로서비스 아키텍처에서 서비스 통합 테스트를 위한 훌륭한 도구입니다.
    
    3. 데모 시스템 구축
    WireMock은 미리 정의된 응답을 가지고 있기 때문에, 데모 시스템을 구축할 때 유용합니다. 예를 들어, WireMock을 사용하여 가짜 결제 시스템을 만들고, 이를 사용하여 결제 처리 흐름을 시뮬레이션할 수 있습니다.
    
    4. 테스트 용이성 개선
    WireMock을 사용하면 테스트 코드를 더욱 쉽게 작성할 수 있습니다. 예를 들어, WireMock을 사용하여 가짜 서버를 띄우고, 이를 이용하여 HTTP 요청을 보내는 코드를 작성할 수 있습니다. 이렇게 작성된 테스트 코드는 외부 서버와의 의존성을 제거할 수 있습니다.
  • 이를 활용하여 외부 서버 통신이 필요한 로직의 테스트의 경우

    • 아래와 같이 url, request, response 를 미리 정의하여 해당 값으로 테스트할 수 있음
    • 외부데이터와의 의존도를 낮출 수 있음
  • json 형태를 미리 정의하거나 WireMock.stubFor 을 통해서도 정의 가능

  • // test/resources/mappings/test.json
    {
    "request" : {
    "url" : "/request",
    "method" : "GET"
    },
    "response" : {
    "status" : 200,
    "body" : "Hello Response1",
    "headers" : {
    "Content-Type" : "text/plain"
    }
    }
    }
  • import com.github.tomakehurst.wiremock.client.WireMock;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
    import org.springframework.http.HttpStatus;
    import org.springframework.test.context.ActiveProfiles;
    import org.springframework.web.client.RestTemplate;

    import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
    import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;


    @SpringBootTest
    @ActiveProfiles("test")
    @AutoConfigureWireMock(port = 9999)
    class WireMockTest {

    private RestTemplate restTemplate = new RestTemplate();

    @Test
    void test_json() {
    String body = "Hello Response1";

    String response = restTemplate.getForObject("http://localhost:9999/request", String.class);

    Assertions.assertEquals(response, body);
    System.out.println(response);
    }

    @Test
    void test_wireMock_method() {
    String body = "Hello Response2";
    stubFor(WireMock.get(urlEqualTo("/request2"))
    .willReturn(
    WireMock.aResponse()
    .withStatus(HttpStatus.OK.value())
    .withHeader("Content-Type", "application/json")
    .withBody("Hello Response2")
    ));

    String response = restTemplate.getForObject("http://localhost:9999/request2", String.class);

    Assertions.assertEquals(response, body);
    }
    }


Testcontainers

  • 테스트 시에 도커 컨테이너를 이용하여 필요한 외부 시스템을 쉽게 생성하고 테스트하는 라이브러리
    • 데이터베이스나 메시지 큐 같은 외부 시스템을 테스트할 때 활용 가능
    • docker-compose 도 사용 가능!
    • 테스트 시에만 도커 컨테이너가 띄워진다고 생각하면 될 듯
    • 하지만 띄우는 시간이 걸림. 테스트가 느려짐. 적절한 용도에 사용하자

// build.gradle
...
testImplementation "org.testcontainers:testcontainers:1.18.0"
testImplementation "org.testcontainers:junit-jupiter:1.18.0"
testImplementation "org.testcontainers:mariadb:1.18.0"
...

// application-test
...
datasource:
url: jdbc:tc:mariadb:10://to-do-list-test
username: admin
password: 1234
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
...

import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.io.File;


@Testcontainers
public class TestContainer {

@Container
public static JdbcDatabaseContainer mariaDBContainer = new org.testcontainers.containers.MariaDBContainer("mariadb:10")
.withDatabaseName("to-do-list-test")
.withUsername("admin")
.withPassword("1234")
//.withConfigurationOverride("conf.d") // DB 서버 추가 설정
.withInitScript("testContainer/initData.sql") // 초기 데이터
;


@Container
static DockerComposeContainer dockerComposeContainer =
new DockerComposeContainer(new File("src/test/resources/testContainer/docker-compose.yml"));

}

(테스트 실행 시 docker ps / docker-compose 에 redis-latest 해놓음 )


참고