
이 시리즈는 TDD를 숙달하기 전에 TEST 자체에 대한 이해를 높이기 위한 학습 시리즈입니다
JUnit5의 라이프 사이클에 TEST에서 사용할 기능을 확장할 때 사용합니다Spring TestContext Framewrok와 JUnit5를 통합해서 사용하게 됩니다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();
}
}
@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를 사용해서 구현했습니다
mailSender의 send 메소드 안에 구현되어 있는 SimpleMailMessage가 동작할 경우 아무것도 일어나지 않게 하는 로직입니다2. 부분을 보겠습니다
UserEntity안에 Id값이 null값이 아닌지 검증했습니다PENDING이 맞는지 검증하고 있습니다UserService에서 유저를 생성하는 부분의 로직 중 인증번호를 생성하는 로직은 userEntity.setCertificationCode(UUID.randomUUID().toString()); 이렇게 구현되어 있습니다3.의 경우도 완전히 2.과 동일합니다
UserService에서 login의 마지막 로그인 시간을 저장하는 부분이 userEntity.setLastLoginAt(Clock.systemUTC().millis()); 이런식으로 구현되어 있기 때문에 도저히 검증할 수가 없습니다Mockito를 활용해서 어찌저찌해서 구현할 수 없다는 뜻이 전혀 아닙니다. 어떻게든 테스트를 통과시킬 순 있을 겁니다. 하지만 이 부분을 과연 테스트를 통과시키는게 더 좋은 프로젝트 설계일까요?Controller 레이어를 테스트하기 위해서 사용하는 어노테이션들입니다MockMvc를 제어하기 위한 설정입니다@MockBean 또는 @Import와 함께 사용되어 Controller 빈에 필요한 객체들을 생성합니다@Controller, @ControllerAdvice 등을 사용할 수 있습니다@Service, @Component, @Repository 등은 사용하지 못합니다@WebMvcTest와 비슷한 기능을 하는 어노테이션입니다@Service나 @Repository가 붙은 객체들도 모두 사용할 수 있습니다@WebMvcTest를 사용해야 합니다MockMvc를 보다 세밀하게 제어하기 위해서 사용하며, 애플리케이션 구성을 로드해서 사용하려는 경우에는 @WebMvcTest보다 @AutoConfigureMockMvc와 @SpringBootTest를 결합해서 사용하는 것을 고려해야 합니다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"));
}
}
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"));
}
}