본문 바로가기

Spring

테스트 코드 작성하기

테스트 작성하는 법

테스트하고 싶은 레이어에 가서 cmd + n 을 누른다!

 

H2가 아닌 다른 데이터베이스에 연결하고 싶을 때

1. 아래 경로에 설정 파일 만들기

 

 

2. 테스트 코드 파일에 어노테이션 2개 추가하기

replace = AutoConfigureTestDatabase.Replace.NONE Spring Boot가 테스트를 위해 다른 데이터베이스(H2 등)로
교체하지 않도록 설정
@TestPropertySource(locations = {"classpath:/application-test.properties"}) 해당 경로의 application.properties를 사용하겠다는 뜻

 

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(locations = {"classpath:/application-test.properties"})

 

Static import 해줄 것

단축키 : art+enter

import static org.assertj.core.api.Assertions.*;

 

Repository 테스트 작성 연습

  • @DataJpaTest
  • 테스트 하고자 하는 객체(repository)에 @Autowired 붙이기

 

참고

Repository 테스트는 작성해도 커버리지가 올라가지 않는다

그리고 사실 검증이 jpa로 잘 됐기 때문에 거의 작성하지 않는다고 한다

 

assertThat

  • isEqualTo : 숫자가 동일한지 비교
  • isSameAs : 문자가 동일한지 비교

 

 

시행착오

만약 댓글 저장 repository를 테스트 할 건데 아래와 같이 코드를 짜면 에러가 난다

comment를 저장하기 위해서는 user와 todo가 저장되어 있어야 하기 때문이다

@DataJpaTest
class CommentRepositoryTest {

    @Autowired
    private CommentRepository commentRepository;
    
    @Test
    public void 댓글이_정상적으로_저장된다() {
        // given
        User user = new User("yn1013@naver.com", "password", UserRole.USER);
        Todo todo = new Todo("title", "contents", "sunny", user);
        Comment comment = new Comment("comments", user, todo);

        // when
        Comment savedComment = commentRepository.save(comment);
    
        // then
        assertThat(comment).isSameAs(savedComment);
        assertThat(comment.getId()).isSameAs(savedComment.getId());
        assertThat(savedComment.getId()).isNotNull();
        assertThat(commentRepository.count()).isEqualTo(1);
    }

}

 

TransientPropertyValueException : Spring Data JPA에서 잘못된 데이터 접근 API 사용 시 발생하는 예외

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation : org.example.expert.domain.comment.entity.Comment.user -> org.example.expert.domain.user.entity.User
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:368)

 

수정된 코드

그래서 아래와 같이 수정해서 짜야 한다! -> user와 todo 또한 @Autowired로 주입한 후에 save를 때려준다

@DataJpaTest
class CommentRepositoryTest {

    @Autowired
    private CommentRepository commentRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TodoRepository todoRepository;
    
    @Test
    public void 댓글이_정상적으로_저장된다() {
        // given
        User user = new User("yn1013@naver.com", "password", UserRole.USER);
        Todo todo = new Todo("title", "contents", "sunny", user);
        Comment comment = new Comment("comments", user, todo);
        userRepository.save(user);
        todoRepository.save(todo);

        // when
        Comment savedComment = commentRepository.save(comment);
    
        // then
        assertThat(comment).isSameAs(savedComment);
        assertThat(comment.getId()).isSameAs(savedComment.getId());
        assertThat(savedComment.getId()).isNotNull();
        assertThat(commentRepository.count()).isEqualTo(1);
    }

}

 

 

조인되서 함께 조회가 되는지 테스트 해보는 코드

@DataJpaTest
class TodoRepositoryTest {

    @Autowired
    private TodoRepository todoRepository;

    @Autowired
    private UserRepository userRepository;

    @Test
    public void Todo와_작성자를_함께_조회한다() {
        // given
        String email = "yn@abc.com";
        User user = new User(email, "password", UserRole.USER);
        userRepository.save(user);

        String weather = "맑음";

        Todo todo = new Todo(
                "할일 제목",
                "할일 내용",
                weather,
                user
        );
        Todo savedTodo = todoRepository.save(todo);

        // when
        Todo foundTodo = todoRepository.findByIdWithUser(savedTodo.getId()).orElse(null);

        // then
        assertNotNull(foundTodo);
        assertEquals("할일 제목", foundTodo.getTitle());
        assertEquals("할일 내용", foundTodo.getContents());
        assertEquals("yn@abc.com", foundTodo.getUser().getEmail());
    }
}

 

저장, 삭제, 조회 테스트 해보는 코드

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void 사용자가_저장된다() {
        // given
        User user = new User("yn1013@naver.com", "password", UserRole.USER);

        // when
        User savedUser = userRepository.save(user);

        // then
        assertThat(user).isSameAs(savedUser);
        assertThat(user.getEmail()).isSameAs(savedUser.getEmail());
        assertThat(savedUser.getId()).isNotNull();
        assertThat(userRepository.count()).isEqualTo(1);
    }

    @Test
    public void 사용자를_id로_조회한다() {
        // given
        User user1 = new User("yn1013@naver.com", "password", UserRole.USER);
        User user2 = new User("yn1122@naver.com", "password", UserRole.USER);
        User savedUser1 = userRepository.save(user1);
        User savedUser2 = userRepository.save(user2);

        // when
        User findUser1 = userRepository.findById(savedUser1.getId())
                .orElseThrow(() -> new InvalidRequestException(ErrorCode.USER_NOT_FOUND));
        User findUser2 = userRepository.findById(savedUser2.getId())
                .orElseThrow(() -> new InvalidRequestException(ErrorCode.USER_NOT_FOUND));

        // then
        assertThat(userRepository.count()).isEqualTo(2);
        assertThat(findUser1.getEmail()).isEqualTo("yn1013@naver.com");
        assertThat(findUser2.getEmail()).isEqualTo("yn1122@naver.com");
    }
    
    @Test
    public void 사용자를_id로_삭제한다() {
        // given
        User user = new User("yn1013@naver.com", "password", UserRole.USER);
        User savedMember = userRepository.save(user);
        Long userId = savedMember.getId();

        // when
        userRepository.deleteById(userId);

        // then
        assertThat(userRepository.count()).isEqualTo(0);
        Optional<User> deletedUser = userRepository.findById(userId);
        assertThat(deletedUser).isEmpty();
    }

    @Test
    public void 이메일로_사용자를_조회할_수_있다() {
        // given
        String email = "yn@abc.com";
        User user = new User(email, "password", UserRole.USER);
        userRepository.save(user);

        // when
        User foundUser = userRepository.findByEmail(email).orElse(null);

        // then
        assertNotNull(foundUser);
        assertEquals(email, foundUser.getEmail());
        assertEquals(UserRole.USER, foundUser.getUserRole());
    }

    @Test
    public void 이메일이_존재하는지_확인할_수_있다() {
        // given
        String email = "yn@abc.com";
        User user = new User(email, "password", UserRole.USER);
        userRepository.save(user);

        // when
        Boolean emailExists = userRepository.existsByEmail(email);

        // then
        assertNotNull(emailExists);
        assertEquals(true, emailExists);
    }


}

 

Service 테스트 작성 연습

  • @ExtendWith(MockitoExtension.class) 붙이기
  • 테스트하고자 하는 객체에 @InjectMocks를 붙이기
  • 그 외의 객체에 @Mock을 붙이기

 

시행착오

테스트 하려는 부분

 

여기서 passwordEncode를 사용하고 있고, 유저 서비스에서도 이것을 private final로 주입받고 있기 때문에 Mock을 주입 해야 함

 

작성된 코드

→ 사실 이러면 틀린다!

  • passwordEncoder.matches()를 모킹하지 않음
  • 실제 passwordEncoder.matches()가 실행되어 예상과 다르게 동작된다

인코딩된 값이 아닌 문자열 자체를 비교해버림!

@Test
    public void 비밀번호_변경시에_기존_비밀번호가_유효하지_않으면_InvalidRequestException을_던진다() {
        // given
        String email = "yn1013@naver.com";
        long userId = 1L;
        User user = new User(email, "password", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);
        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        String oldPassword = "oldPassword";
        String newPassword = "newPassword";
        UserChangePasswordRequest request = new UserChangePasswordRequest(oldPassword, newPassword);

        // when & then
        InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> userService.changePassword(userId, request));
        assertEquals("비밀번호가 잘못되었습니다.", exception.getErrorCode().getMessage());
    }

 

그래서 수정된 코드!

전체 서비스 메서드 코드

 

리포지토리에서 사용자 조회하는 부분

@Test
    public void User를_Id로_조회할_수_있다() {
        // given
        String email = "yn1013@naver.com";
        long userId = 1L;
        User user = new User(email, "password", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);

        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        // when
        UserResponse userResponse = userService.getUser(userId);

        // then
        assertThat(userResponse).isNotNull();
        assertThat(userResponse.getId()).isEqualTo(userId);
        assertThat(userResponse.getEmail()).isEqualTo(email);
    }
 @Test
    public void 존재하지_않는_User_조회시에_InvalidRequestException을_던진다() {
        // given
        long userId = 1L;
        given(userRepository.findById(anyLong())).willReturn(Optional.empty());

        // when & then
        InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> userService.getUser(userId));
        assertEquals("유저가 존재하지 않습니다.", exception.getErrorCode().getMessage());
    }

 

첫 번째 예외 처리 부분

@Test
    public void 비밀번호_변경시에_기존_비밀번호와_동일하면_InvalidRequestException을_던진다() {
        // given
        String email = "yn1013@naver.com";
        long userId = 1L;
        User user = new User(email, "password", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);

        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        String oldPassword = "oldPassword";
        String newPassword = "oldPassword";
        UserChangePasswordRequest request = new UserChangePasswordRequest(oldPassword, newPassword);

        given(passwordEncoder.matches(request.getNewPassword(), user.getPassword())).willReturn(true);

        // when & then
        InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> userService.changePassword(userId, request));
        assertEquals("이미 사용 중인 비밀번호 입니다.", exception.getErrorCode().getMessage());
    }

 

두번째 예외처리 부분

@Test
    public void 비밀번호_변경시에_기존_비밀번호가_유효하지_않으면_InvalidRequestException을_던진다() {
        // given
        String email = "yn1013@naver.com";
        long userId = 1L;
        User user = new User(email, "password", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);
        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        String oldPassword = "oldPassword";
        String newPassword = "newPassword";
        UserChangePasswordRequest request = new UserChangePasswordRequest(oldPassword, newPassword);

        given(passwordEncoder.matches(request.getNewPassword(), user.getPassword())).willReturn(false);
        given(passwordEncoder.matches(request.getOldPassword(), user.getPassword())).willReturn(false);

        // when & then
        InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> userService.changePassword(userId, request));
        assertEquals("비밀번호가 잘못되었습니다.", exception.getErrorCode().getMessage());
    }

 

비밀번호 검증 부분

@Test
    public void 비밀번호가_성공적으로_변경된다() {
        // given
        long userId = 1L;
        User user = new User("yn1013@naver.com", "oldPassword", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);

        String oldPassword = "oldPassword";
        String newPassword = "newPassword";
        UserChangePasswordRequest request = new UserChangePasswordRequest(oldPassword, newPassword);

        // 서비스 내부에서 findById 실행
        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));
        // 첫 번째 검증 로직 통과하도록 설정
        given(passwordEncoder.matches(request.getNewPassword(), user.getPassword())).willReturn(false);
        // 두 번째 검증 로직 통과하도록 설정
        given(passwordEncoder.matches(request.getOldPassword(), user.getPassword())).willReturn(true);
        // changePassword 안에 들어갈 값 설정
        given(passwordEncoder.encode(request.getNewPassword())).willReturn("encodedNewPassword");

        // when
        userService.changePassword(userId, request);

        // then
        assertEquals("encodedNewPassword", user.getPassword());
    }

 

이제 잘 돌아간다!

 

Controller 테스트 작성 연습

1. `@WebMvcTest(XxxController.class)`를 붙인다.

2. @MockBean 혹은 @MockitoBean을 붙인다. (전자는 3.4 이전, 후자는 3.4 이후)

3. API 호출은 아래의 MockMvc를 사용

 

테스트할 부분

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    public void user_단건_조회() throws Exception {
        // given
        long userId = 1L;
        String email = "yn1013@naver.com";

        BDDMockito.given(userService.getUser(userId)).willReturn(new UserResponse(userId, email));

        // when & then
        mockMvc.perform(get("/users/{userId}", userId))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(userId))
                .andExpect(MockMvcResultMatchers.jsonPath("$.email").value(email));
    }
}

 

컨트롤러 에러난 경우 테스트

아래 로그를 보고 똑같이 맞춰주면 된다

@Test
    public void user_단건_조회_시_user가_존재하지_않아_예외가_발생한다() throws Exception {
        // given
        long userId = 1L;

        // when
        Mockito.when(userService.getUser(userId))
                .thenThrow(new InvalidRequestException(ErrorCode.USER_NOT_FOUND));

        // then
        mockMvc.perform(get("/users/{userId}", userId))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value(HttpStatus.BAD_REQUEST.value())) //숫자
                .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.name())) //문자
                .andExpect(jsonPath("$.message").value("유저가 존재하지 않습니다."));
    }

 

리스트 조회 테스트

@Test
    void User_목록_조회_빈리스트() throws Exception {
        // given
        given(userService.getUsers()).willReturn(List.of());

        // when & then
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isEmpty());
    }

    @Test
    void User_목록_조회() throws Exception {
        // given
        long userId1 = 1L;
        long userId2 = 2L;
        String email1 = "user1@a.com";
        String email2 = "user2@a.com";
        List<UserResponse> userList = List.of(
                new UserResponse(userId1, email1),
                new UserResponse(userId2, email2)
        );
        given(userService.getUsers()).willReturn(userList);

        // when & then
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[0].id").value(userId1))
                .andExpect(jsonPath("$[0].email").value(email1))
                .andExpect(jsonPath("$[1].id").value(userId2))
                .andExpect(jsonPath("$[1].email").value(email2));
    }