레이어별 테스트

Gyeongjae Ham·2023년 6월 22일
0

TEST

목록 보기
4/7
post-thumbnail

이 시리즈는 TDD를 숙달하기 전에 TEST 자체에 대한 이해를 높이기 위한 학습 시리즈입니다

Repository

@ExtendWith

  • JUnit5의 라이프 사이클에 TEST에서 사용할 기능을 확장할 때 사용합니다

@ExtendWith(SpringExtension.class)

  • Spring TestContext FramewrokJUnit5를 통합해서 사용하게 됩니다

@ExtendWith(MockitoExtension.class)

  • Mockito와 관련된 MockContext 기반에서 가볍게 테스트를 진행할 수 있습니다
@ExtendWith(SpringExtension.class)
@DataJpaTest(showSql = true)
// TEST 환경에서 사용할 설정 파일의 위치를 지정합니다
@TestPropertySource("classpath:test-application.properties")
// TEST 환경에서 사용할 데이터 값을 넣는 sql 파일의 위치입니다
@Sql("/sql/user-repository-test-data.sql")
class UserRepositoryTest {

	// Repository를 테스트하기 위해서 Bean에 등록합니다
    @Autowired
    private UserRepository userRepository;

    @Test
    void findByIdAndStatus로_유저_데이터를_찾아올_수_있다() {
        // Given
        // When
        Optional<UserEntity> result = userRepository.findByIdAndStatus(1, UserStatus.ACTIVE);

        // Then
        assertThat(result.isPresent()).isTrue();
    }

    @Test
    void findByIdAndStatus_는_데이터가_없으면_Optional_empty_를_내려준다() {
        // Given
        // When
        Optional<UserEntity> result = userRepository.findByIdAndStatus(1, UserStatus.PENDING);

        // Then
        assertThat(result.isEmpty()).isTrue();
    }

    @Test
    void findByEmailAndStatus로_유저_데이터를_찾아올_수_있다() {
        // Given
        // When
        Optional<UserEntity> result = userRepository.findByEmailAndStatus("kok2020@naver.com", UserStatus.ACTIVE);

        // Then
        assertThat(result.isPresent()).isTrue();
    }

    @Test
    void findByEmailAndStatus_는_데이터가_없으면_Optional_empty_를_내려준다() {
        // Given
        // When
        Optional<UserEntity> result = userRepository.findByEmailAndStatus("kok2020@naver.com", UserStatus.PENDING);

        // Then
        assertThat(result.isEmpty()).isTrue();
    }

}

Service

@SpringBootTest
@TestPropertySource("classpath:test-application.properties")
@SqlGroup({
        @Sql(value = "/sql/user-service-test-data.sql", executionPhase = BEFORE_TEST_METHOD),
        @Sql(value = "/sql/delete-all-data.sql", executionPhase = AFTER_TEST_METHOD)
})
class UserServiceTest {
	
    // 테스트할 UserService를 Bean에 등록해줍니다
    @Autowired
    private UserService userService;
    
    // JavaMailSender를 Mock 객체로 Bean에 등록해줍니다
    // 실제 비즈니스 로직에 있는 JavaMailSender의 구현체를 대신하는 대역입니다
    @MockBean
    private JavaMailSender mailSender;

    @Test
    void getByEmail은_ACTIVE_상태인_유저를_찾아올_수_있다() {
        // Given
        String email = "kok2020@naver.com";

        // When
        UserEntity result = userService.getByEmail(email);

        // Then
        assertThat(result.getNickname()).isEqualTo("kok202");
    }

    @Test
    void getByEmail은_PENDING_상태인_유저를_찾아올_수_없다() {
        // Given
        String email = "kok303@naver.com";

        // When
        // Then
        assertThatThrownBy(() -> {
            UserEntity result = userService.getByEmail(email);
        }).isInstanceOf(ResourceNotFoundException.class);
    }

    @Test
    void getById은_ACTIVE_상태인_유저를_찾아올_수_있다() {
        // Given
        // When
        UserEntity result = userService.getById(1);

        // Then
        assertThat(result.getNickname()).isEqualTo("kok202");
    }

    @Test
    void getById은_PENDING_상태인_유저를_찾아올_수_없다() {
        // Given
        // When
        // Then
        assertThatThrownBy(() -> {
            UserEntity result = userService.getById(2);
        }).isInstanceOf(ResourceNotFoundException.class);
    }

//========================================================================
    @Test
    void userCreateDto를_이용하여_유저를_생성할_수_있다() {
        // Given
        UserCreateDto userCreateDto = UserCreateDto.builder()
                .email("kok2020@kakao.com")
                .address("Gyeongi")
                .nickname("kok202-k")
                .build();
        // 1.
		// 이 부분이 없다면 이 테스트는 실패를 하게 됩니다
        // 실제 UserService를 동작시켰는데 메일을 전송하지 못했기 때문이죠
        // 실제 UserService 비즈니스 로직안에는 메일을 전송하는 로직이 있습니다
        // 그 기능이 동작했다고 설정하기 위해서 BDDMockito를 사용해서 해결했습니다
		BDDMockito.doNothing().when(mailSender).send(any(SimpleMailMessage.class));

        // When
        UserEntity result = userService.create(userCreateDto);

        // Then
        assertThat(result.getId()).isNotNull();
        assertThat(result.getStatus()).isEqualTo(UserStatus.PENDING);
        
        // 2. ???
//        assertThat(result.getCertificationCode()).isEqualTo("????");
    }
//========================================================================

    @Test
    void user를_로그인_시키면_마지막_로그인_시간이_변경된다() {
        // Given
        // When
        userService.login(1);

        // Then
        UserEntity userEntity = userService.getById(1);
        // 3.
        assertThat(userEntity.getLastLoginAt()).isGreaterThan(0L);
//        assertThat(userEntity.getLastLoginAt()).isEqualTo("........");

}
  • service 테스트를 보면 1. 부분에서 실제 기능이 동작한 것처럼 BDDMockito를 사용해서 구현했습니다

    • mailSendersend 메소드 안에 구현되어 있는 SimpleMailMessage가 동작할 경우 아무것도 일어나지 않게 하는 로직입니다
  • 2. 부분을 보겠습니다

    • 이 테스트에서 원하는대로 결과가 나왔는지 검증하고 있는 부분들 중 마지막입니다
    • 위에서부터 유저가 정상적으로 생성되었으면 반환된 UserEntity안에 Id값이 null값이 아닌지 검증했습니다
    • 처음 생성된 유저는 이메일 인증전까지 가져야하는 상태가 PENDING이 맞는지 검증하고 있습니다
    • 그리고 생성된 인증번호 값을 검증하고 싶어집니다
      • 하지만 우리는 이 부분을 전혀 검증할 수 없습니다
      • UserService에서 유저를 생성하는 부분의 로직 중 인증번호를 생성하는 로직은 userEntity.setCertificationCode(UUID.randomUUID().toString()); 이렇게 구현되어 있습니다
      • 값이 랜덤으로 생성되기 때문에 전혀 이 부분을 검증할 수 없습니다
      • 테스트가 보내는 명확한 신호인 부분입니다 추후 설계를 고치면서 해결하기로 하고 우선 넘어가도록 하겠습니다
  • 3.의 경우도 완전히 2.과 동일합니다

    • UserService에서 login의 마지막 로그인 시간을 저장하는 부분이 userEntity.setLastLoginAt(Clock.systemUTC().millis()); 이런식으로 구현되어 있기 때문에 도저히 검증할 수가 없습니다
    • 여기서 검증할 수 없다는건 Mockito를 활용해서 어찌저찌해서 구현할 수 없다는 뜻이 전혀 아닙니다. 어떻게든 테스트를 통과시킬 순 있을 겁니다. 하지만 이 부분을 과연 테스트를 통과시키는게 더 좋은 프로젝트 설계일까요?
    • 이 부분 역시 테스트가 설계에 문제가 있다고 신호를 보내고 있는 부분입니다

Controller

MockMvc

  • 웹 어플리케이션을 서버에 배포하지 않고 테스트용으로 MVC 환경을 만들어 요청 및 전송, 응답기능을 제공해주는 클래스입니다

@WebMvcTest, @AutoConfigureMockMvc

  • Controller 레이어를 테스트하기 위해서 사용하는 어노테이션들입니다
  • 둘 다 MockMvc를 제어하기 위한 설정입니다

@WebMvcTest

  1. 웹에서 테스트하기 힘든 컨트롤러를 테스트하는데 적합한 어노테이션입니다
  2. 웹 상에서 요청과 응답에 대해 테스트할 수 있고, 시큐리티 혹은 필터까지 자동으로 테스트해서 수동으로 추가/삭제가 가능합니다
    • 일반적으로 @MockBean 또는 @Import와 함께 사용되어 Controller 빈에 필요한 객체들을 생성합니다
  3. 선언할 경우, @Controller, @ControllerAdvice 등을 사용할 수 있습니다
    • @Service, @Component, @Repository 등은 사용하지 못합니다

@AutoConfigureMockMvc

  • @WebMvcTest와 비슷한 기능을 하는 어노테이션입니다
  • 가장 큰 차이점은 컨트롤러 뿐만 아니라 테스트 대상이 아닌 @Service@Repository가 붙은 객체들도 모두 사용할 수 있습니다
  • 더 가볍고 간단한 테스트를 위해서는 @WebMvcTest를 사용해야 합니다
  • MockMvc를 보다 세밀하게 제어하기 위해서 사용하며, 애플리케이션 구성을 로드해서 사용하려는 경우에는 @WebMvcTest보다 @AutoConfigureMockMvc@SpringBootTest를 결합해서 사용하는 것을 고려해야 합니다

UserController 예시

package com.example.demo.controller;

import com.example.demo.model.UserStatus;
import com.example.demo.model.dto.UserUpdateDto;
import com.example.demo.repository.UserEntity;
import com.example.demo.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlGroup;
import org.springframework.test.web.servlet.MockMvc;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METHOD;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureTestDatabase
@TestPropertySource("classpath:test-application.properties")
@SqlGroup({
        @Sql(value = "/sql/user-controller-test-data.sql", executionPhase = BEFORE_TEST_METHOD),
        @Sql(value = "/sql/delete-all-data.sql", executionPhase = AFTER_TEST_METHOD)
})
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private UserRepository userRepository;
    // json 변환
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void 사용자는_특정_유저의_개인정보는_소거된_정보를_전달_받을_수_있다() throws Exception {
        // Given
        // When
        // Then
        // MockMvcRequestBuilders.get
        mockMvc.perform(get("/api/users/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.email").value("kok2020@naver.com"))
                .andExpect(jsonPath("$.nickname").value("kok202"))
                .andExpect(jsonPath("$.address").doesNotExist())
                .andExpect(jsonPath("$.status").value("ACTIVE"));
    }

    @Test
    void 사용자는_존재하지_않는_유저의_아이디로_api_호출할_경우_404_응답을_받는다() throws Exception {
        // Given
        // When
        // Then
        mockMvc.perform(get("/api/users/112312321"))
                .andExpect(status().isNotFound())
                .andExpect(content().string("Users에서 ID 112312321를 찾을 수 없습니다."));
    }

    @Test
    void 사용자는_인증_코드로_계정을_활성화_시킬_수_있다() throws Exception {
        // Given
        // When
        // Then
        mockMvc.perform(get("/api/users/2/verify")
                        .queryParam("certificationCode", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"))
                .andExpect(status().isFound());

        UserEntity userEntity = userRepository.findById(2L).get();
        assertThat(userEntity.getStatus()).isEqualTo(UserStatus.ACTIVE);
    }

    @Test
    void 사용자는_인증_코드가_일치하지_않을_경우_권한_없음_에러를_내려준다() throws Exception {
        // Given
        // When
        // Then
        mockMvc.perform(get("/api/users/2/verify")
                        .queryParam("certificationCode", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaac"))
                .andExpect(status().isForbidden());

    }

    @Test
    void 사용자는_내_정보를_불러올_때_개인정보인_주소도_갖고_올_수_있다() throws Exception {
        // Given
        // When
        // Then
        mockMvc.perform(get("/api/users/me")
                        .header("EMAIL", "kok2020@naver.com"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.email").value("kok2020@naver.com"))
                .andExpect(jsonPath("$.nickname").value("kok202"))
                .andExpect(jsonPath("$.address").value("Seoul"))
                .andExpect(jsonPath("$.status").value("ACTIVE"));
    }

    @Test
    void 사용자는_내_정보를_수정할_수_있다() throws Exception {
        // Given
        UserUpdateDto userUpdateDto = UserUpdateDto.builder()
                .nickname("kok202-n")
                .address("Pangyo")
                .build();
        // When
        // Then
        mockMvc.perform(put("/api/users/me")
                        .header("EMAIL", "kok2020@naver.com")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(userUpdateDto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.email").value("kok2020@naver.com"))
                .andExpect(jsonPath("$.nickname").value("kok202-n"))
                .andExpect(jsonPath("$.address").value("Pangyo"))
                .andExpect(jsonPath("$.status").value("ACTIVE"));
    }

}

UserCreateController 예시

  • Servie 예제와 같이 mailSender의 역할을 MockBean으로 Mock 객체가 대신 수행하도록 했습니다
package com.example.demo.controller;

import com.example.demo.model.dto.UserCreateDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlGroup;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureTestDatabase
@SqlGroup({
        @Sql(value = "/sql/delete-all-data.sql", executionPhase = AFTER_TEST_METHOD)
})
public class UserCreateControllerTest {

    @Autowired
    private MockMvc mockMvc;
    // mailSender로 인증 메일 전송하는 부분 Mock 객체로 대역
    @MockBean
    JavaMailSender mailSender;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void 사용자는_회원_가입을_할_수있고_회원가입된_사용자는_PENDING_상태이다() throws Exception {
        // Given
        UserCreateDto userCreateDto = UserCreateDto.builder()
                .email("kok202@kakao.com")
                .nickname("kok202")
                .address("Pangyo")
                .build();
        BDDMockito.doNothing().when(mailSender).send(any(SimpleMailMessage.class));

        // When
        // Then
        mockMvc.perform(
                        post("/api/users")
                                .header("EMAIL", "kok202@naver.com")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(objectMapper.writeValueAsString(userCreateDto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").isNumber())
                .andExpect(jsonPath("$.email").value("kok202@kakao.com"))
                .andExpect(jsonPath("$.nickname").value("kok202"))
                .andExpect(jsonPath("$.status").value("PENDING"));
    }

}
profile
Always be happy 😀

0개의 댓글