Framework/Spring

[Test] Spring Boot 환경에서의 유닛테스트와 통합테스트, 슬라이스 테스트

Joonfluence 2024. 9. 19. 23:51

서론

오늘은 Spring Boot 환경에서 유닛 테스트와 통합 테스트, 각각의 테스트 과정에서 활용되는 어노테이션과 메서드들은 무엇이 있는지 정리해보겠습니다.

대상독자

테스트코드를 처음 작성해보시는 분들, Slice Test를 처음 들어보시는 분들, 테스트코드를 하고 싶지만 심리적인 장벽 때문에 아직 시도해보지 못한 분들에게 이 글을 추천합니다.

이론

유닛 테스트(Unit test)와 통합 테스트 (Integration test)

꼭 짚고 넘어가야 하는 유닛 테스트와 통합 테스트의 정의에 대해서 알아봅시다.

유닛 테스트

  • 보통 개발자가 테스트 코드를 짠다고 하면, 유닛 테스트 짜는 것을 말합니다. 코드 레벨에서의 테스트를 하며, 특정 함수나 클래스와 같이 작고 고립된 코드의 기능을 확인하는 자동화된 테스트를 진행합니다.

통합 테스트

  • 보통 통합 테스트라 함은 최종 사용자가 프로그램을 사용하는 것처럼(블랙박스 테스트), 기능 동작을 위해 필요한 모듈 전체를 테스트하는 것을 말합니다. 가령, 특정 API 테스트를 한다고하면 서버 실행 및 서버와 통신하는 DB, Cache 등 연관된 모듈 간 통신을 실제로 함으로써, 실제 실행환경과 동일하게 실행했을 때 정상 동작 되는지 보는거죠. 이를 통해, 유닛 테스트에서는 확인할 수 없는 모듈, 구성 요소간의 상호 작용과 호환성도 확인할 수 있습니다.

슬라이스 테스트 (Slice test)

Spring MVC에서 한 가지 빼놓을 수 없는 테스트가 있습니다. 바로 슬라이스 테스트입니다. 보통 Spring MVC로 웹 어플리케이션 서버를 개발하면 Entity, Dto, Repository와 Servcie, Controller로 레이어가 구분됩니다. 슬라이스 테스트에선 각 레이어를 구분해서 살펴봅니다.

슬라이스 테스트

  • 스프링 공식 문서에 따르면, "Test slicing is about segmenting the ApplicationContext that is created for your test. Typically, if you want to test a controller using MockMvc, surely you don't want to bother with the data layer. Instead you'd probably want to mock the service that your controller uses and validate that all the web-related interaction works as expected. This can be summarized in the example below." 슬라이스 테스트란 레이어를 기준으로 테스트하고자 하는 대상을 좁힘으로써, 해당 레이어에서 수행하는 역할에 대해서만 테스트하는 것을 말합니다.
  • 아래 예시처럼, Controller 계층을 슬라이스 테스트하는 예시가 있습니다. 굳이 Service 계층까지 테스트할 필요가 없기 때문에 @MockBean으로 등록하고, Service 계층의 getVehicleDetails 메서드의 응답 값을 미리 지정해둘 수 있을 것입니다.
@WebMvcTest(UserVehicleController.class)
public class UserVehicleControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    public 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"));
    }
}

Unit Test와 Slice Test, Unit Test와 Integration Test가 과연 차이가 있을까?

사실, 위의 내용처럼 실제 작성한 유닛 테스트와 슬라이스 테스트, 더 나아가 유닛 테스트와 통합 테스트가 과연 실제 코드 상에서 항상 유의미한 차이가 있을까요? 저는 그 차이가 때론 미미하다고 생각합니다. 유닛 테스트여도 Controller의 동작에 집중해서 보면, Controller 계층에 대한 슬라이스 테스트가 되기도 합니다. 유닛 테스트여도 전체 동작의 일부를 파악하기 위해, 통합 테스트처럼 테스트 환경을 마련해야 하기도 합니다.

테스팅 원칙과 각 테스트의 장점과 단점 비교

각 테스트들은 서로 다른 장점과 단점을 가지고, 상호보완적인 성격을 갖기 때문에 각각을 적재적소에 잘 활용하는 것이 좋습니다. 이를 위해, 테스트 코드의 대가들이 이야기하는 테스팅 원칙에 대해 알아보고, 각 테스트의 장점과 단점을 비교해보겠습니다.

일곱 테스팅 원칙 (Seven Testing Principles)

  • 테스팅은 결함의 존재를 보여주는 것이다.
  • 완벽한 테스트는 불가능하다.
  • 테스트 구성은 가능한 빠르게 시작한다.
  • 결함은 군집되어 있다.
  • 살충제 역설(Pesticide Paradox) : 비슷한 테스트가 반복되면 새로운 결함을 발견할 수 없다.
  • 테스팅은 문맥에 의존적이다.
  • 오류 부재의 궤변: 사용되지 않는 시스템이나 사용자의 기대에 부응하지 않는 기능의 결함을 찾고 수정하는 것은 의미가 없다.

F.I.R.S.T 원칙

F.I.R.S.T 원칙은 로버트 밥 마틴이 2008년 클린 코드에서 제안한 유닛 테스트 작성 원칙입니다.

Fast : 유닛 테스트는 빨라야 한다.

유닛 테스트는 속도가 빨라야 합니다. 그렇지 않으면 개발/도입 시간이 느려지고 통과하거나 실패하는 데 더 오랜 시간이 걸립니다. 몇 천 개의 단위 테스트가 있다고 가정했을 때, 만약 평균적인 단위 테스트가 실행되는 데 200밀리초가 걸린다면 (빠른 것으로 간주됩니다), 전체 제품군을 실행하는 데는 대략 5-10분 정도 소요됩니다. 이 시간이 길지 않아 보이지만 하루에 여러 번 통합 및 배포된다면, 그 때마다 이 시간이 소요됩니다. 그리고 애플리케이션에 새로운 기능이 추가되면 테스트 실행 시간이 더 늘어날 수 있습니다. 따라서 Mockito와 같은 라이브러리를 사용하여 외부 의존성을 만드는 것을 피해야 합니다. 느린 테스트의 주요 원인들 중 하나는 데이터베이스, 파일, 네트워크 통화와 같은 I/O 작업이기 때문입니다.

Isolated : 다른 테스트에 종속적인 테스트는 절대로 작성하지 않는다.

다른 테스트에 의존적인 테스트는 테스트 결과를 불완전하게 만들기 때문입니다. 외부 요인으로 인해 테스트가 통과되거나, 통과되지 않는다면 그 테스트는 잘못된 테스트입니다. 또한 독립적인 테스트를 작성해야 하는 두번째 이유는 테스트 실패 시, 원인을 명확하게 파악할 수 있도록 하여 코드 디버깅에 필요한 시간을 줄일 수 있기 때문입니다.

Repeatable : 테스트는 실행할 때마다 같은 결과를 만들어야 한다.

테스트는 실행할 때마다 동일한 결과를 내야 합니다. 이를 보장하려면, 외부 환경에 의존하는 모든 작업들을 격리해야 합니다. 주로 Mock 객체를 사용합니다. 이를 통해, 외부 환경으로부터 독립적이고 반복 수행하더라도 테스트가 통과됨을 보장해줄 수 있습니다.

Self-validating : 테스트는 스스로 결과물이 옳은지 그른지 판단할 수 있어야 한다.

유닛 테스트를 자동화하기 위해선, 테스트 코드를 실행했을 때 성공 혹은 실패 여부를 판별할 수 있어야 합니다.

Timely/Thorough

  • Timely : 보통 TDD에 따라 개발 할 때, 테스트하려는 코드를 작성하기 이전에 유닛 테스트를 먼저 작성하라고 합니다. 그랬을 때, 더 깨끗하고 테스트 가능한 코드를 작성하기 쉬워지기 때문입니다.
  • Thorugh : Timely가 아닌, Through로 보기도 합니다. 우리가 어떤 방법을 테스트할 때 성공이 예상되는 테스트 뿐만 아니라, 오류가 발생 가능한 경우와 부정적인 경로에 대해서도 작성해줘야 합니다.

저는 Timely를 따르던, Through를 따르던, 둘 다 따르던 큰 상관은 없다고 생각합니다. 더 중요한 것은 이 원칙들을 가이드라인 삼아, 실제 테스트코드 작성 시에 도움이 될 수 있게 활용하는 것이기 때문이죠.

유닛 테스트

먼저, 앞서 언급한 테스팅 원칙을 준수한 테스트임을 가정합니다. 테스트 코드를 작성했을 때 얻을 수 있는 이점, 그리고 단점은 따로 다루지 않았습니다.

  • 장점
    • 빠릅니다. 테스트 범위가 제한되기 때문에, 빠를 수 밖에 없습니다.
    • 자동화할 수 있습니다. 앞서 설명한 유닛 테스트의 Self-validating 속성과 연관됩니다.
  • 단점
    • 테스트를 작성하는데 시간이 소요됩니다. 테스트 대상에 대한 설계가 잘못 될 경우, 또한 테스트 코드를 잘못 작성하게 됩니다.
    • 실제 실행 결과의 성공을 보장하지 않습니다. 유닛 테스트는 외부 모듈 간 상호 작용을 테스트하지 않습니다. 따라서 유닛 테스트가 통과한다고 해서, 항상 테스트에 통과하는 것은 아닙니다.

통합 테스트

  • 장점
    • 실제 실행 결과의 성공을 보장합니다.
  • 단점
    • 느립니다.
    • 항상 성공할 수 없습니다.

슬라이스 테스트

슬라이스 테스트는 장점과 단점을 비교하는 것보다, 각 계층 별 테스트를 어느 상황에서 수행하면 좋을지에 관한 의견을 드리는 것으로 갈음해보려고 합니다.

  • Repository
    • 복잡한 조회 쿼리 (동적쿼리 쓸 때) 작성하고 테스트할 때 쓰면 좋습니다. 가령 아래와 같이, JPQL을 통해 검색 기능을 구현하기 위해 동적쿼리를 작성했다고 해봅시다. 이런 복잡한 쿼리를 테스트하기 위해, Repository 테스트를 진행하면 좋습니다.
     String jpql = "SELECT DISTINCT new ResponseDto(...) FROM ...";
     String whereSql = " where ";
     List<String> whereCondition = new ArrayList<>();
     List<String> orCondition = new ArrayList<>();
    
    if(조건1 != null){
        whereCondition.add("조건1 = :파라미터1");
    }
    
    ...
    
    if(조건10 != null){
        whereCondition.add("조건1 = :파라미터10");
    }
    
    
long totalCount = query.getResultList().size();
List<ChipResponseDto> associationChipRegistereds = query.setFirstResult(offset).setMaxResults(pageSize).getResultList();
return new PageImpl<>(associationChipRegistereds, pageable, totalCount);
```

Service

  • 필수적으로 외부 서버와 통신하여 처리해야 하는 로직 테스트 할 때, 활용합니다. ex) DB, Cache Server, 메일, 카카오톡 발송

Controller

  • Controller 역할에 따라, 주로 예외처리/Validation 등을 테스트하려고 할 때 사용합니다. Service 계층에서의 동작은 given 메서드로 Mocking 할 수 있습니다.

실습

Unit Test 방법

이번에는 실제 코드 상에서 실행해보며, 테스트해보겠습니다.

테스트 환경설정

먼저 Test를 하기 전에 해야 할 작업이 있습니다. 테스트 DB 환경을 설정해줍니다. /src/main/resources 다렉토리 아래에, application-test.yaml 파일을 적어주겠습니다.

spring:
  config:
    activate:
      on-profile: "test"
  output:
    ansi:
      enabled: ALWAYS
  datasource:
    url: jdbc:h2:mem:starbucks
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate: # hibernate 사용 설정
      ddl-auto: create-drop
    properties: # property 사용 설정
      hibernate: # hibernate property 설정
        show-sql: true
        format_sql: true

Repository 테스트 방법

앞서 "test"로 명명한 profile을 활성화시키기 위해, @ActiveProfiles("test")를 추가해줍니다. 또 @DataJpaTest를 활용합니다. 테스트 이후 자동 롤백해주는 기능과 테스트 DB에 대한 환경설정을 처리해주는 역할을 담당합니다. 그런 뒤, 의존성을 주입하고자 하는 Repository에 @Autowired를 추가해줍니다.

@DataJpaTest
@ActiveProfiles("test")
class CustomerRepositoryTest {
    @Autowired
    private CustomerRepository repository;
}

그런 뒤, 요청에 쓰일 엔티티를 미리 정의해둡니다. 이렇게 미리 정의해두는 까닭은, 테스트 할 떄 공통적으로 쓰기 위함입니다.

class CustomerRepositoryTest {
    private static Customer dbInsertedUser;
    private static Customer newUserRequest;

    @BeforeAll
    public static void init(){
        Double random = Math.random();
        String userId = "joonluence.dev" + random.toString() + "@gmail.com";
        dbInsertedUser = Customer.builder().name("Junho").email(userId).password("12341234").build();
        newUserRequest = Customer.builder().name("Junho").email("joonfluence.dev2@gmail.com").password("12341234").build();
    }
}

마지막으로 테스트 코드를 작성해줍니다. 검증 방법은 요청 값과 응답 값을 비교하는 방식으로 처리해줍니다.

class CustomerRepositoryTest {

    ...

    @DisplayName("1. repository의 Save 메서드 호출 시, 정상적으로 저장되어야 한다.")
    @Test
    void test_1(){
        // given & when
        Customer saved = repository.save(newUserRequest);

        // then
        Assertions.assertNotNull(saved);
        Assertions.assertEquals(saved.getName(), newUserRequest.getName());
        Assertions.assertEquals(saved.getPassword(), newUserRequest.getPassword());
    }

    @DisplayName("2. repository의 findById 호출 시, 정상적으로 호출되어야 한다.")
    @Test
    void test_2(){
        // given & when
        Customer foundCustomer = repository.findById(dbInsertedUser.getId()).orElseThrow(() -> new NoSuchElementException("존재하지 않습니다"));
        // then
        Assertions.assertEquals(foundCustomer.getName(), dbInsertedUser.getName());
        Assertions.assertEquals(foundCustomer.getPassword(), dbInsertedUser.getPassword());
    }
}

실행 해보면, 위와 같이 테스트가 정상적으로 통과되는 것을 볼 수 있습니다.

Service 테스트

Service 레이어 테스트부터는 본격저긍로 Mockito를 활용해서 유닛 테스트를 진행합니다. DB 서버와의 통신 작업과 같은 외부 작업을 Mocking 해줍니다. 이렇게 하는 까닭은 테스트를 독립적으로 작성하기 위함입니다. 외부 서버로 요청을 보냈다가 응답 받는 과정 자체를 생략이 가능해집니다. 또 테스트 범위가 줄어들어, 잘못 작성한 로직 파악이 쉬워지게 됩니다.

아래와 같이, AuthenticationService에서 register, 회원가입 로직을 테스트해보겠습니다.

@Slf4j
@RequiredArgsConstructor
@Service
public class AuthenticationService {
    private final CustomerRepository repository;
    private final RefreshTokenService refreshTokenService;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;
    private final AuthenticationManager authenticationManager;

    @Transactional
    public RegisterResponse register(RegisterRequest request) {
        validateByEmail(request.getEmail());
        Customer customer = request.toEntity();
        customer.updatePassword(passwordEncoder.encode(customer.getPassword()));
        Customer saved = repository.save(customer);
        return new RegisterResponse(saved.getId(), saved.getName(), saved.getEmail());
    }
}

Mockito를 사용하여 테스트 할 것이므로 @ExtendWith(MockitoExtension.class)을 추가해줍니다. 테스트하고자 하는 대상인 authenticationService에 @InjectMocks을 추가해줍니다. 그리고 의존성으로 주입되어야 할 요소들에 @Mock 어노테이션을 추가해줍니다.

@ExtendWith(MockitoExtension.class)
@ActiveProfiles("test")
class AuthenticationServiceTest {
    @Mock
    JwtService jwtService;
    @Mock
    AuthenticationManager authenticationManager;
    @Mock
    PasswordEncoder passwordEncoder;
    @Mock
    CustomerRepository customerRepository;
    @Mock
    Authentication authentication;
    @InjectMocks
    AuthenticationService authenticationService;

그리고 RequestDto, Token 값 등 필요한 값들을 미리 셋팅을 해줍니다.

class AuthenticationServiceTest {
    private RegisterRequest registerRequestDto;
    private RegisterRequest blankRegisterRequestDto;
    private Customer user;
    private LoginRequest loginRequest;
    private AuthenticationResponse response;

    @BeforeEach
    public void init(){
        registerRequestDto = RegisterRequest.builder().email("joonfluence.dev@gmail.com").name("Joonho").password("!abcd1234").build();
        blankRegisterRequestDto = RegisterRequest.builder().email("").name("Joonho").password("12341234").build();
        user = registerRequestDto.toEntity();
        loginRequest = LoginRequest.builder().email("joonfluence.dev@gmail.com").password("12341234").passwordRepeated("12341234").build();
        response = AuthenticationResponse.builder().accessToken("eyJhbGciOiJIUzUxMiJ9.eyJhdXRoIjoiUk9MRV9VU0VSIiwic3ViIjoiNyIsImlhdCI6MTcwMTY4NjgwNCwiZXhwIjoxNzAxNjg4NjA0fQ.VlSs4U8ferPP8Uh5QmumVmeO_OgRMwk8YK7_lSOAY5kFY3Hos1u14FvQNQQ3b_spTLSpsZOYOx7Rx5tgBL-95Q").refreshTokenUUid(UUID.randomUUID().toString()).build();
    }
}

그리고 아래와 같이, given/when/then 로직에 따라, 요청에 따른 테스트코드 결과 값을 정의해줍니다.

class AuthenticationServiceTest {
    @DisplayName("1. 사용자가 회원가입에 필요한 정보를 입력했을 때, 정상 가입되어야 한다.")
    @Test
    void register() {
        // given : 사용자가 회원가입에 필요한 정보를 입력했을 때
        when(customerRepository.save(Mockito.any(Customer.class))).thenReturn(user);

        // when
        RegisterResponse registeredUser = authenticationService.register(registerRequestDto);
        Optional<Customer> customer = customerRepository.findById(registeredUser.getUserId());

        // then
        Assertions.assertNotNull(customer);
    }
}

실제 실행해보면 정상적으로 통과되는 것을 볼 수 있습니다.

Controller 테스트

마지막으로 Controller 테스트를 진행해봅시다. 테스트 환경을 마련하기 위해, @WebMvcTest를 추가해주고 모든 HTTP Request 요청 때마다 거치는 Filter를 테스트 환경에서는 제외해줍니다. 그런 뒤, MockSpringSecurityFilter를 추가해줍니다.

@WebMvcTest(
        controllers = {AuthrizationController.class},
        excludeFilters = {
                @ComponentScan.Filter(
                        type = FilterType.ASSIGNABLE_TYPE,
                        classes = { SecurityConfiguration.class, JwtAuthenticationFilter.class }
                )
        }
)
@ActiveProfiles("test")
class AuthrizationControllerTest {
    private MockMvc mockMvc;
    @MockBean
    private AuthenticationService authenticationService;
    private Principal mockPrincipal;
    @Autowired
    private WebApplicationContext context;
    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(springSecurity(new MockSpringSecurityFilter()))
                .build();
        init();
    }
}

SecurityContextHolder에 Authentication을 등록하는 기본적인 동작만 수행하는 Test 환경에서 활용되는 필터를 추가해줍니다.

public class MockSpringSecurityFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        SecurityContextHolder.getContext()
                .setAuthentication((Authentication) ((HttpServletRequest) req).getUserPrincipal());
        chain.doFilter(req, res);
    }

    @Override
    public void destroy() {
        SecurityContextHolder.clearContext();
    }
}

그리고 MockMvc(Postman 같은 일종의 클라이언트)를 활용해서 실제 요청을 보내보고, ResponseDto로 요청 값을 확인해봅니다. 아래와 같이, 성공 시 응답 값이 존재할 때, 그 값을 미리 정의해줄 수 있습니다.

public class AuthrizationController {
    @PostMapping("/signUp")
    public ResponseEntity<GlobalResponse<RegisterResponse>> signUp(@RequestBody @Valid RegisterRequest request){
        RegisterResponse response = authenticationService.register(request);
        return ResponseEntity.status(201).body(new GlobalResponse<RegisterResponse>(201, response, "회원가입이 완료되었습니다."));
    }
}
class AuthrizationControllerTest {
    @DisplayName("1. 사용자가 회원가입에 정보를 입력했을 때, 정상 가입되어야 한다.")
    @Test
    void registered_when_user_submit_right_value() throws Exception {
        // given : 사용자가 회원가입에 필요한 정보를 입력했을 때
        RegisterResponse responseDto = new RegisterResponse(1L, registerRequestDto.getName(), registerRequestDto.getEmail());
        given(authenticationService.register(registerRequestDto)).willReturn(responseDto);

        // when
        ResultActions response = mockMvc.perform(post("/api/v1/auth/signUp")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequestDto)));

        // then
        response.andExpect(MockMvcResultMatchers.status().isCreated())
                .andExpect(MockMvcResultMatchers.jsonPath("$.message", CoreMatchers.is("회원가입이 완료되었습니다.")))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.userId", CoreMatchers.is(responseDto.getUserId().intValue())))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.name", CoreMatchers.is(responseDto.getName())))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.email", CoreMatchers.is(responseDto.getEmail())));
    }
}

실제 실행해보면, 통과되는 것을 볼 수 있습니다.

제가 작성한 유닛 테스트 코드들은 성공하는 케이스에 대해서만 검증했기 때문에, 완전한(Through) 테스트라고 보긴 어렵습니다. 이메일 중복 회원 가입 여부, PW 잘못 입력했을 때의 경우 등 다양한 유즈케이스에 대한 테스트코드가 추가로 작성되어야 함을 미리 말씀 드립니다.

통합 테스트 방법

통합테스트는 @SpringBootTest를 활용해서 테스트 해볼 수 있습니다. 실제 스프링 컨테이너를 실행 후, 이를 통해 의존성을 주입 받을 것이기 때문에 아래와 같이, 코드를 적어줍니다. 그 외에는 Controller 테스트와 동일한 형태임을 확인해보실 수 있을 것 입니다.

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class DemoApplicationIntegerationTests {
    @Autowired
    AuthrizationController authrizationController;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private MockMvc mockMvc;

    private RegisterRequest registerRequestDto;

    @BeforeEach
    public void setup() {
        init();
    }

    public void init(){
        registerRequestDto = RegisterRequest.builder().email("joonfluence.dev@gmail.com").name("Joonho").password("!abcd1234").build();
    }

    @DisplayName("회원가입을 진행할 때, 정상 가입되어야 한다.")
    @Test
    void test() throws Exception {
        // given : 사용자가 회원가입에 필요한 정보를 입력했을 때
        RegisterResponse responseDto = new RegisterResponse(1L, registerRequestDto.getName(), registerRequestDto.getEmail());

        // when
        ResultActions response = mockMvc.perform(post("/api/v1/auth/signUp")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequestDto)));

        MvcResult result = response.andReturn();
        System.out.println("result = " + result.getResponse().getContentAsString());

        // then
        response.andExpect(MockMvcResultMatchers.status().isCreated())
                .andExpect(MockMvcResultMatchers.jsonPath("$.message", CoreMatchers.is("회원가입이 완료되었습니다.")))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.userId", CoreMatchers.is(responseDto.getUserId().intValue())))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.name", CoreMatchers.is(responseDto.getName())))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.email", CoreMatchers.is(responseDto.getEmail())));
    }
}

실습코드

실제 위 내용을 적용한 코드들은 아래 프로젝트 코드에서 확인해보실 수 있습니다. 다음 번에는 해당 기능을 구현한 실제 코드들을 가지고 조금 더 자세히 이야기해보도록 하겠습니다. 감사합니다.

https://github.com/joonfluence/starbucks-coffee-order-app

결론

테스트하려는 목적에 따라, 테스트 방법은 다양합니다. 개발자인 저희는 어떤 상황에 놓일 지 예측하기 어려우므로, 평소에 테스트코드를 작성하는 방법에 관해 알아보고 연습해보고 정리해둘 필요가 있겠습니다.

레퍼런스

https://spring.io/blog/2016/08/30/custom-test-slice-with-spring-boot-1-4
https://tech.buzzvil.com/handbook/test-principles/
https://howtodoinjava.com/best-practices/first-principles-for-good-tests/
https://github.com/cheese10yun/spring-guide/blob/master/docs/test-guide.md#%EB%8B%A8%EC%A0%90
https://tecoble.techcourse.co.kr/post/2021-05-18-slice-test/
https://meetup.nhncloud.com/posts/124

반응형