테스트 작성하는 법
테스트하고 싶은 레이어에 가서 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));
}
'Spring' 카테고리의 다른 글
조회수 및 어뷰징 방지 Redis로 구현하기 (0) | 2025.03.30 |
---|---|
failed to create jar file 문제 해결하기 (1) | 2025.03.25 |
JPA와 Transaction (1) | 2025.03.21 |
Spring Security의 JWT 적용 (1) | 2025.03.21 |
User 데이터 100만건 생성하고 성능 개선하기 (1) | 2025.03.21 |