저번 포스팅에서 JPA Repository Test Code를 작성하는 것까지 포스팅을 했었습니다.
Service Test Code 또한 가장 기본적인 Given - When - Then 패턴을 가지고 작성할 것이기 때문에 저번 게시글과 중복되는 부분도 꽤 있어서 간략히 작성할 예정입니다.
이전 Repository Test Code 작성 부분이 궁금하신 분들은 -> 이곳에서 보실 수 있습니다.
자! 그러면 지금 부터 Service Test Code 작성을 시작해보겠습니다.
Service 클래스의 메서드들을 테스트 하기 전에 Service 클래스를 먼저 작성해보겠습니다.
Service 클래스와 Repository 클래스는 하는 역할이 다릅니다. Service 클래스는 복잡한 비즈니스 로직을 수행하고 Transaction을 다룹니다.
Service와 Repository의 차이와 계층을 분리하는 이유에 대해서는 나중에 자세히 다뤄보도록 하겠습니다.
Service 클래스의 코드는 아래와 같습니다.
@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/*
* 회원 가입
* @param User
* @return userNo
* */
@Transactional
public User join(User user) {
isDuplicate(user);
user.setUserRole(UserRole.USER);
return userRepository.save(user);
}
/*
* 회원 한명 조회
* @param userNo
* @return User
* */
public User findOneUser(Long userNo) {
Optional<User> findUser = userRepository.findById(userNo);
findUser.orElseThrow(() -> new IllegalStateException("조회하려는 회원은 존재하지 않습니다."));
return findUser.get();
}
/*
* 회원 한명 이름으로 조회
* @param userNo
* @return User
* */
public User findOneUserByName(String name) {
return userRepository.findByName(name);
}
/*
* 회원 한명 업데이트
* @param User
* @return userNo
* */
@Transactional
public User modifyUser(User user) {
User modifyUser = userRepository.save(user);
return modifyUser;
}
/*
* 회원 한명 삭제
* @param userNo
* */
@Transactional
public void removeUser(Long userNo) {
User user = userRepository.findById(userNo)
.orElseThrow(() -> new IllegalStateException("해당 회원번호와 일치하는 회원이 없어 삭제가 불가능합니다."));
userRepository.delete(user);
}
/*
* 중복 회원 조회 메서드
* @param User
* @return boolean
* */
private boolean isDuplicate(User user) {
User findUser = userRepository.findByName(user.getName());
if (findUser != null) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
} else {
return true;
}
}
}
일단 클래스에 @Transactional(readOnly = true), @Service, @RequiredArgsConstructor를 붙여주었습니다.
여기서 핵심적인 애너테이션은 @Transactional 입니다.
@RequiredArgsConstructor를 붙이는 이유는 Lombok을 사용해서 조금 더 쉽게UserRepository를 생성자 주입으로 주입받기 위해서 입니다.
생성자 주입, 필드 주입, Setter 주입과 같은 주입 방식에 대해서는 이곳을 참고하시면 됩니다.
일단 @Transactional을 readOnly로 해놓은 이유는 @Transactional 애너테이션의 readOnly는 default가 false입니다. 현재 이 서비스 클래스는 조회(Read)하는 메서드가 데이터를 변경하는 쓰기기능(Update,Insert,Delete)보다 많기때문에 기본적으로 조회를 위해 readOnly 속성을 주고, 쓰기 작업을 하는 메서드 위에만 @Transactional 애너테이션을 붙여주었습니다.
회원을 등록, 수정, 조회, 삭제하는 기본적인 CRUD 메서드와 회원 가입시 이름을 기준으로 중복회원인지 체크해주는 isDuplicate 메서드를 가지고 있습니다.
코드를 보시면 단순하게 작성이 되어있기 때문에 따로 뜯어서 설명해놓지는 않겠습니다.
이제 진짜 Test Code를 작성하고 하나하나 뜯어 보겠습니다.
저번 Repository Test와 비슷한 내용은 가볍게 뜯어보고 넘어가도록 하겠습니다.
@Test
@DisplayName("정상 회원 가입 테스트")
@Transactional
public void joinUser() {
// given
User insertUser = userService.join(User.builder()
.id("Kafka")
.pwd("javascript")
.email("aaa@aaa.com")
.name("RexSeo")
.userRole(UserRole.USER)
.build());
// when
User findUser = userService.findOneUser(insertUser.getNo());
// then
assertThat(insertUser).isEqualTo(findUser);
}
정상 회원가입의 경우 테스트 코드를 간단하게 작성 해보았습니다.
한명의 회원을 Insert하고, 가입한 회원의 PK값을 이용해서 다시 회원 한명을 조회하였을때 회원가입 할때 사용한 User 엔티티(insertUser)와 가입한 회원의 PK값을 이용해서 조회해온 User 엔티티(findUser)가 같다면 테스트는 성공하게 됩니다.
JPA에서는 같은 트랜잭션 안에서 엔티티의 동일성을 보장받기 때문에 이렇게 테스트를 하게 됩니다.
@Test
@DisplayName("중복 회원 가입 테스트")
@Transactional
public void joinDuplicateUser() {
// given
User insertUser = User.builder()
.id("Kafka")
.pwd("javascript")
.email("aaa@aaa.com")
.name("RexSeo")
.userRole(UserRole.USER)
.build();
// when
userService.join(insertUser);
// then
assertThatThrownBy(() -> userService.join(insertUser))
.isInstanceOf(IllegalStateException.class);
}
중복회원 가입의 경우에 UserService에서 중복 회원가입 시도시 터지는 예외가 잘 발생하는지 확인하는 시나리오로 테스트 코드를 작성하였습니다.
UserService 클래스의 join 메서드는 내부에서 isDuplicate 메서드를 호출하게 됩니다.
isDuplicate 메서드는 현재 회원가입하려는 User 정보 중 이름을 사용해서 User를 조회하고, 해당 User가 현재 조회가 된다면 중복 유저이기 때문에 IllegalStateException을 발생 시킵니다.
따라서 User 한명을 가입시키고, 같은 User 엔티티로 중복가입을 시도했을때 IllegalStateException이 발생하면 테스트가 성공합니다.
when 절에서 예외를 발생시키고 그 예외를 반환받아, then 절에서 isInstanceOf로 검증하는 것이 문맥에 조금더 맞아 보이지만, 메서드 체이닝 방식으로 코드를 간결하고 예쁘게 작성하려다 보니 when 절에서 최초 회원가입 메서드를 실행하도록 작성하게 되었습니다.
@Test
@DisplayName("회원 수정 테스트")
@Transactional
public void modifyUserTest() {
// given
User insertUser = User.builder()
.id("Kafka")
.pwd("javascript")
.email("aaa@aaa.com")
.name("RexSeo")
.userRole(UserRole.USER)
.build();
insertUser = userService.join(insertUser);
User modifyUser = User.builder()
.no(insertUser.getNo())
.id("Kafka")
.pwd("javascript")
.email("aaa@aaa.com")
.name("DevSeo")
.userRole(UserRole.USER)
.build();
// when
User modifiedUser = userService.modifyUser(modifyUser);
User findModifiedUser = userService.findOneUserByName("DevSeo");
//then
assertThat(modifiedUser).isEqualTo(findModifiedUser);
}
회원 수정 테스트 코드는 회원을 한명 가입시키고, 회원 가입된 User의 정보를 변경하는 기능을 테스트 하도록 작성 했습니다.
회원 가입을 하고, 회원 가입때 사용한 User 엔티티의 PK값을 이용해 새로운 User 엔티티를 만들어주고 엔티티 내부의 값을 변경합니다.
그리고 변경할 User의 정보를 담은 엔티티를 save 메서드를 이용해 Update 해준다음 User의 이름으로 User 한명을 조회합니다.
최초 User 이름은 "RexSeo" 였는데 User의 이름이 "DevSeo"인지 확인하여 맞다면 Update 된것이므로 테스트가 성공합니다.
@Test
@DisplayName("회원 삭제 테스트")
@Transactional
public void deleteUserTest() {
// given
User insertUser = userService.join(User.builder()
.id("Kafka")
.pwd("javascript")
.email("aaa@aaa.com")
.name("RexSeo")
.userRole(UserRole.USER)
.build());
// when
userService.removeUser(insertUser.getNo());
// then
assertThatThrownBy(() -> userService.findOneUser(insertUser.getNo()))
.isInstanceOf(IllegalStateException.class);
}
드디어 마지막! 회원 삭제 테스트 코드 입니다.
회원 삭제는 간단합니다 회원 한명을 가입시키고, 해당 회원을 삭제한 뒤에 삭제된 회원의 PK 값으로 회원 한명을 조회해서 예외가 발생하면 테스트가 성공합니다.
회원이 이미 탈퇴했는데 해당 회원을 조회 했을때 예외가 발생하지 않으면 정상적으로 프로그램이 동작했다고 볼 수 없기 때문에 예외가 발생하는지 여부로 테스트 결과를 판단하게 되는 코드를 작성했습니다.
아무래도 프로젝트 할때 테스트 코드를 한번도 짜본적이 없고, 어떻게 짜야할지 블로그만 돌아다니면서 실습을 하지 않았었기 때문에 아무래도 어려움이 처음에 많았습니다.
또 아직 어색하기도 하고, Given - When - Then 패턴으로 기본적인 틀을 잡아가는 것 같습니다.
실무 레벨에서 하는 TDD는 아니지만 그래도 테스트 코드를 작성할때 로직의 흐름을 생각하게 되었고, 손부터 올려서 기능만 뽑아내는 코딩을 개인프로젝트에서라도 하지 말아야 하겠다는 생각이 들었다는 것에서 매우 만족하고 있답니다!