이 시리즈는 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"));
}
}