Service Test의 맹점
이전에 Repository 테스트 코드를 Embedded DB를 활용하여 작성하는 방법에 대해 포스팅했었습니다. 오늘은 Repository를 의존하고 있는 Service를 어떻게 테스트할 수 있는지 알아보겠습니다.
@DataMongoTest의 단점
@DataMongoTest는 @SpringBootTest와 함께 사용할 수 없을 뿐만 아니라, 테스트 코드 작성 시 의존성 주입이 까다롭다는 단점이 있습니다. @DataMongoTest를 사용하지 않고 진행하려 해도, MongoRepository<Domain, ID>를 상속받은 Repository 구현체가 필요하기 때문에 의존성 문제가 발생하게 됩니다.
이 문제를 해결하기 위한 방법은 무엇일까요?
해결책: Mock 사용하기
Mock은 빈 구현체를 만들어서 주입하는 방식으로, 실제 Repository를 대체하여 테스트할 수 있게 해줍니다. 이를 자동으로 처리해주는 프레임워크로는 Mockito가 있는데, 이 프레임워크의 사용법에 대해서는 이후 포스팅에서 다루도록 하겠습니다.
Service 코드
다음은 프로젝트의 Service 클래스 코드입니다. 이 클래스는 Repository를 의존하고 있으며, 아래의 joinNewUserFromDefault 메서드를 통해 새 사용자를 저장하는 역할을 합니다.
@Slf4j @Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
private final ValidateUserData repository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Transactional
public API<Object> joinNewUserFromDefault(UserJoinDto dto) {
String encodePassword = bCryptPasswordEncoder.encode(dto.getPassword());
UserData newOffspring = UserData.builder()
.email(dto.getEmail())
.userName(dto.getUserName())
.password(encodePassword)
.provider(SsoType.DEFAULT)
.userType(UserType.OFFSPRING).build();
repository.saveNewUserData(newOffspring);
}
return new API<>(APISuccessMessage.회원가입_성공);
}
}
Service Test 코드 (Mock 없이 작성한 경우)
다음은 @DataMongoTest를 사용하여 작성한 테스트 코드입니다. 이 경우, @Bean에 대한 검증이나 관련 테스트를 작성하기 어려운 단점이 있습니다.
@DataMongoTest
class UserServiceTest {
private UserService userService;
UserServiceTest(@Autowired UserDataRepository repository) {
ValidateUserData repo = new ValidateUserData(repository);
this.userService = new UserService(repo, new BCryptPasswordEncoder());
}
@Test
void 신규가입_확인() {
UserJoinDto dto = new UserJoinDto("admin@gmail.com", "admin", "1234qwer", "OFFSPRING", "DEFAULT");
API<Object> result = userService.joinNewUserFromDefault(dto);
assertThat(result.getApiMessage().getMessage()).isEqualTo(APISuccessMessage.회원가입_성공.getMessage());
}
}
Service Test 코드 (Mock으로 작성한 경우)
위의 문제를 해결하기 위해 Mock을 사용하여 테스트 코드를 작성할 수 있습니다. 아래는 Mock을 이용한 테스트 코드입니다.
class UserServiceTest {
private UserService userService;
UserServiceTest() {
ValidateUserData repository = new ValidateUserData(new MockUserDataRepository());
this.userService = new UserService(repository, new BCryptPasswordEncoder());
}
@Test
void 신규가입_확인() {
UserJoinDto dto = new UserJoinDto("admin@gmail.com", "admin", "1234qwer", "OFFSPRING", "DEFAULT");
API<Object> result = userService.joinNewUserFromDefault(dto);
assertThat(result.getApiMessage().getMessage()).isEqualTo(APISuccessMessage.회원가입_성공.getMessage());
}
}
MockUserDataRepository 구현체
아래는 MockUserDataRepository 구현체입니다. 이 클래스는 MongoDBRepository를 상속받아 구현하며, 메서드들을 모두 Override하면 된다.
더보기
더보기
MockUserDataRepository 구현체이다. MongoDBRepository를 상속받아서 구현하였으므로, 매우 길고 복잡하지만, 그냥 Override를 받아서 처리하면 된다. 필요한 리턴 타입은 따로 정의하면 될 것 같다.
public class MockUserDataRepository implements UserDataRepository {
public MockUserDataRepository() {
super();
}
@Override
public Optional<UserData> findByEmail(String email) {
return Optional.empty();
}
@Override
public void deleteByEmail(String email) {
}
@Override
public <S extends UserData> S insert(S entity) {
return null;
}
@Override
public <S extends UserData> List<S> insert(Iterable<S> entities) {
return List.of();
}
@Override
public <S extends UserData> List<S> findAll(Example<S> example) {
return List.of();
}
@Override
public <S extends UserData> List<S> findAll(Example<S> example, Sort sort) {
return List.of();
}
@Override
public <S extends UserData> List<S> saveAll(Iterable<S> entities) {
return List.of();
}
@Override
public List<UserData> findAll() {
return List.of();
}
@Override
public List<UserData> findAllById(Iterable<String> strings) {
return List.of();
}
@Override
public <S extends UserData> S save(S entity) {
return null;
}
@Override
public Optional<UserData> findById(String s) {
return Optional.empty();
}
@Override
public boolean existsById(String s) {
return false;
}
@Override
public long count() {
return 0;
}
@Override
public void deleteById(String s) {
}
@Override
public void delete(UserData entity) {
}
@Override
public void deleteAllById(Iterable<? extends String> strings) {
}
@Override
public void deleteAll(Iterable<? extends UserData> entities) {
}
@Override
public void deleteAll() {
}
@Override
public List<UserData> findAll(Sort sort) {
return List.of();
}
@Override
public Page<UserData> findAll(Pageable pageable) {
return null;
}
@Override
public <S extends UserData> Optional<S> findOne(Example<S> example) {
return Optional.empty();
}
@Override
public <S extends UserData> Page<S> findAll(Example<S> example, Pageable pageable) {
return null;
}
@Override
public <S extends UserData> long count(Example<S> example) {
return 0;
}
@Override
public <S extends UserData> boolean exists(Example<S> example) {
return false;
}
@Override
public <S extends UserData, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
return null;
}
}
Mock을 사용하면 추가적인 프레임워크나 설정 없이 테스트를 진행할 수 있습니다. 작은 프로젝트의 경우 이런 방식으로 테스트를 작성하는 것이 유용할 수 있습니다.
혹시라도 수정할 부분이 있다면 댓글로 알려주세요!