Home > TDD(Test-Driven Development) > 단위 테스트

단위 테스트
java

이전 글들을 모두 읽고 오자 !!

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