이전 글들을 모두 읽고 오자 !!
Dependencies
- Spring Boot 3.4.1
- Junit 5.10.5
- Mockito
단위 테스트 예시
0. 테스트에 사용될 객체들
@Entity
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email")
private String email;
@Column(name = "password")
private String password;
@Column(name = "age")
private int age;
@Builder
public User(String email, String password, int age) {
this.email = email;
this.password = password;
this.age = age;
}
public void changeAge(int age) {
this.age = age;
}
}
public interface UserRepository extends Repository<User,Long> {
User save(User user);
Optional<User> findByEmail(String email);
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public Long signUp(String email, String password, int age) {
userRepository.findByEmail(email)
.ifPresent(user -> throw new ExpectedException(ErrorCode.ALREADY_EXISTED_USER););
User user = User.builder()
.email(email)
.password(password)
.age(age)
.build();
return userRepository.save(user).getId();
}
}
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// 회원 가입
@PostMapping("/signup")
public ResponseEntity<Long> signUp(@RequestBody UserSignUpRequest signUpRequest) {
Long userId = userService.signUp(signUpRequest.getEmail(),
signUpRequest.getPassword(),
signUpRequest.getAge());
return new ResponseEntity<>(userId, HttpStatus.CREATED);
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserSignUpRequest {
private String email;
private String password;
private int age;
public UserSignUpRequest(String email, String password, int age) {
this.email = email;
this.password = password;
this.age = age;
}
}
@Getter
@RequiredArgsConstructor
public class ExpectedException extends RuntimeException {
private final ErrorCode errorCode;
}
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
ALREADY_EXISTED_USER("ALREADY_EXISTED_USER", "이미 존재하는 유저입니다.");
private final String errorCode;
private final String errorMessage;
}
1. Domain 단위 테스트
public class UserTest {
@Test
@DisplayName("유저 생성")
void createUser() {
// when
User user = User.builder().email("joyuri@gmail.com").password("yuri123").age(20).build();
// then
assertThat(user.getEmail()).isEqualTo("joyuri@gmail.com");
assertThat(user.getPassword()).isEqualTo("yuri123");
assertThat(user.getAge()).isEqualTo(20);
}
@Test
@DisplayName("유저 나이 변경")
void changeUserAge() {
// given
User user = User.builder().email("joyuri@gmail.com").password("yuri123").age(20).build();
// when
user.changeAge(25);
// then
assertThat(user.getAge()).isEqualTo(25);
}
}
2. Service 단위 테스트
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
@DisplayName("회원 가입 성공")
void signUp_Success() {
// given
User userArgs = User.builder().email("joyuri@gmail.com").password("yuri123").age(20).build();
User userResult = User.builder()
.email(userArgs.getEmail())
.password(userArgs.getPassword())
.age(userArgs.getAge())
.build();
ReflectionTestUtils.setField(userResult, "id", 3L);
BDDMockito.given(userRepository.save(userArgs))
.willReturn(userResult); // Stub
// when
Long userId = userService.signUp(userArgs.getEmail(), userArgs.getPassword(), userArgs.getAge());
// then
assertThat(userId).isEqualTo(3L);
}
@Test
@DisplayName("회원 가입 시 이미 존재하는 유저이면 예외 발생")
void signUp_AlreadyExistedUser_Then_Exception() {
// given
User existingUser = User.builder().email("joyuri@gmail.com").password("yuri123").age(20).build();
BDDMockito.given(userRepository.findByEmail(existingUser.getEmail()))
.willReturn(Optional.of(existingUser)); // Stub
// when & then
assertThatThrownBy(() -> userService.signUp(existingUser.getEmail(), existingUser.getPassword(), existingUser.getAge()))
.isInstanceOf(ExpectedException.class)
.satisfies(e -> {
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTED_USER);
}
);
}
}
3. Controller 단위 테스트 & Repository 단위 테스트
- Domain과 Service의 경우에는 별 다른 설정 없이 작성한 코드, 즉 순수 로직을 테스트할 수 있었다. 그러나, Controller와 Repository의 경우에는 어떨까?
- Controller를 테스트하기 위해서는 웹(Spring MVC) 관련 컴포넌트들이 필요하다. 이를 위해
@WebMvcTest
어노테이션을 사용한다.- Repository를 테스트하기 위해서는 (JPA를 사용할 경우) JPA 관련 컴포넌트들이 필요하다. 이를 위해
@DataJpaTest
어노테이션을 사용한다.- @WebMvcTest, @DataJpaTest를 사용하는 테스트는 Spring Context의 특정 부분을 로드한다고 해서
슬라이스 테스트(Slice Test)
라고 불리긴 하지만, 개인적으로는단위 테스트
라고 부르고자 한다.
단위 테스트
는 일반적으로 한 클래스나 한 메서드를 테스트하는 것을 의미한다. @WebMvcTest 또는 @DataJpaTest는 일부라도 Spring Context를 로드하기 때문에 테스트 범위가 더 넓어 보일 수 있지만, 본질적으로는 특정 클래스(Controller나 Repository)를 테스트하는 것이기 때문에 단위 테스트로 간주할 수 있다고 생각한다.
3-1. Controller 단위 테스트
@WebMvcTest(UserController.class)
public class UserControllerTest {
@MockBean // @Mock은 Spring Context에 등록되지 않으므로 userService의 의존성이 주입되지 않음
private UserService userService;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("회원 가입 성공")
void signUp_Success() {
// given
UserSignUpRequest signUpRequest
= new UserSignUpRequest("joyuri@gmail.com", "yuri123", 20);
BDDMockito.given(userService.signUp(signUpRequest.getEmail(),
signUpRequest.getPassword(),
signUpRequest.getAge()))
.willReturn(3L);
// when & then
mockMvc.perform(post("/api/users/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(signUpRequest)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$").value(3L));
}
}
3-2. Repository 단위 테스트
@DataJpaTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("유저 저장")
void saveUser() {
// given
User newUser = User.builder().email("joyuri@gmail.com").password("yuri123").age(20).build();
// when
User savedUser = userRepository.save(newUser);
// then
assertThat(savedUser.getId()).isGreaterThan(0L);
assertThat(savedUser.getEmail()).isEqualTo(newUser.getEmail());
}
@Test
@DisplayName("이메일로 유저 찾기")
void findUserByEmail() {
// given
User existingUser = User.builder().email("joyuri@gmail.com").password("yuri123").age(20).build();
userRepository.save(existingUser);
// when
Optional<User> foundUser = userRepository.findByEmail("joyuri@gmail.com");
// then
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getEmail()).isEqualTo(existingUser.getEmail());
assertThat(foundUser.get().getAge()).isEqualTo(existingUser.getAge());
}
}
- 참고 자료
테스트 주도 개발 시작하기(저자: 최범균)
https://martinfowler.com/articles/practical-test-pyramid.html