Service 소형 테스트로 전환

Gyeongjae Ham·2023년 7월 15일
0

TEST

목록 보기
7/7
post-thumbnail

이 시리즈는 TDD를 숙달하기 전에 TEST 자체에 대한 이해를 높이기 위한 학습 시리즈입니다

기존 UserServiceTest 코드를 보면 SpringBootTest이고, h2 데이터베이스를 사용하며, 관련해서 설정해주기 위해서 @TestPropertySource, @SqlGroup 등의 설정을 이용해서 테스트를 하고 있습니다

외부 기능에 대해서는 @MockBean으로 등록해주고 있는 상황입니다. 이 테스트를 구동해보면 알겠지만 테스트를 실행할 때, Spring boot가 올라가고, h2 데이터베이스가 올라가는 시간이 걸려서 꽤 오랜시간이 걸리게 됩니다. 이 정도 테스트 시간이면 얼마 거리지 않는다고 생각한다면 우리는 겨우 한 도메인의 서비스 레이어를 테스트하는 시간이라는 걸 떠올려야 합니다. 실제로 개발한 서비스에는 훨씬 많은 도메인과 더 많은 서비스 레이어가 존재하므로, 리팩토링이나 새로운 기능을 추가할 때, 테스트 작성은 물론 테스트를 돌리면서 검증하는 시간 자체에 두려움을 느끼기에 충분할 겁니다

@SpringBootTest
@TestPropertySource("classpath:test-application.properties")
@SqlGroup({
        @Sql(value = "/sql/user-service-test-data.sql", executionPhase = BEFORE_TEST_METHOD),
        @Sql(value = "/sql/delete-all-data.sql", executionPhase = AFTER_TEST_METHOD)
})
class UserServiceTest {

    @Autowired
    private UserService userService;
    @MockBean
    private JavaMailSender mailSender;

    @Test
    void getByEmail은_ACTIVE_상태인_유저를_찾아올_수_있다() {
        // Given
        String email = "kok2020@naver.com";

        // When
        User result = userService.getByEmail(email);

        // Then
        assertThat(result.getNickname()).isEqualTo("kok202");
    }

    @Test
    void getByEmail은_PENDING_상태인_유저를_찾아올_수_없다() {
        // Given
        String email = "kok303@naver.com";

        // When
        // Then
        assertThatThrownBy(() -> {
            User result = userService.getByEmail(email);
        }).isInstanceOf(ResourceNotFoundException.class);
    }

    @Test
    void getById은_ACTIVE_상태인_유저를_찾아올_수_있다() {
        // Given
        // When
        User result = userService.getById(1);

        // Then
        assertThat(result.getNickname()).isEqualTo("kok202");
    }

    @Test
    void getById은_PENDING_상태인_유저를_찾아올_수_없다() {
        // Given
        // When
        // Then
        assertThatThrownBy(() -> {
            User result = userService.getById(2);
        }).isInstanceOf(ResourceNotFoundException.class);
    }

    @Test
    void userCreateDto를_이용하여_유저를_생성할_수_있다() {
        // Given
        UserCreate userCreate = UserCreate.builder()
                .email("kok2020@kakao.com")
                .address("Gyeongi")
                .nickname("kok202-k")
                .build();
        BDDMockito.doNothing().when(mailSender).send(any(SimpleMailMessage.class));

        // When
        User result = userService.create(userCreate);

        // Then
        assertThat(result.getId()).isNotNull();
        assertThat(result.getStatus()).isEqualTo(UserStatus.PENDING);
//        assertThat(result.getCertificationCode()).isEqualTo("????"); // FIXME
    }

    @Test
    void userUpdateDto_를_이용하여_유저를_수정할_수_있다() {
        // Given
        UserUpdate userUpdate = UserUpdate.builder()
                .address("Incheon")
                .nickname("kok202-n")
                .build();

        // When
        userService.update(1, userUpdate);

        // Then
        User user = userService.getById(1);
        assertThat(user.getId()).isNotNull();
        assertThat(user.getAddress()).isEqualTo("Incheon");
        assertThat(user.getNickname()).isEqualTo("kok202-n");
    }

    @Test
    void user를_로그인_시키면_마지막_로그인_시간이_변경된다() {
        // Given
        // When
        userService.login(1);

        // Then
        User user = userService.getById(1);
        assertThat(user.getLastLoginAt()).isGreaterThan(0L);
//        assertThat(user.getLastLoginAt()).isEqualTo("........"); // FIXME
    }

    @Test
    void PENDING_상태의_사용자는_인증_코드로_ACTIVE_시킬_수_있다() {
        // Given
        // When
        userService.verifyEmail(2, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab");

        // Then
        User user = userService.getById(2);
        assertThat(user.getStatus()).isEqualTo(UserStatus.ACTIVE);
    }

    @Test
    void PENDING_상태의_사용자는_잘못된_인증_코드를_받으면_에러를_던진다() {
        // Given
        // When
        // Then
        assertThatThrownBy(() -> {
            userService.verifyEmail(2, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaad");
        }).isInstanceOf(CertificationCodeNotMatchedException.class);
    }

}

앞 선 외부 기능을 interface를 사용해서 의존성을 약화 시키는 작업을 똑같이 Repository에도 적용했습니다. service 패키지 아래의 port에는 UserRepository 인터페이스가 있으며 infrastructure 아래에는 구현된 UserRepositoryImpl이 존재하는 상황입니다. 이 경우는 post 도메인의 경우에도 마찬가지로 적용되어 있다고 생각하고 진행하겠습니다

우선 테스트의 mock 패키지 아래에 UserServiceTest에서 사용할 FakeUserRepository를 만들어주겠습니다

public class FakeUserRepository implements UserRepository {

    private final AtomicLong autoGeneratedId = new AtomicLong(0);
    private final List<User> data = new ArrayList<>();

    @Override
    public Optional<User> findById(final long id) {
        return data.stream().filter(item -> item.getId().equals(id)).findAny();
    }

    @Override
    public Optional<User> findByIdAndStatus(final long id, final UserStatus userStatus) {
        return data.stream().filter(item -> item.getId().equals(id) && item.getStatus() == userStatus).findAny();
    }

    @Override
    public Optional<User> findByEmailAndStatus(final String email, final UserStatus userStatus) {
        return data.stream().filter(item -> item.getEmail().equals(email) && item.getStatus() == userStatus).findAny();
    }

    @Override
    public User save(User user) {
        if (user.getId() == 0 || user.getId() == null) {
            User newUser = User.builder()
                    .id(autoGeneratedId.incrementAndGet())
                    .email(user.getEmail())
                    .nickname(user.getNickname())
                    .address(user.getAddress())
                    .certificationCode(user.getCertificationCode())
                    .status(user.getStatus())
                    .lastLoginAt(user.getLastLoginAt())
                    .build();
            data.add(newUser);
            return newUser;
        } else {
            data.removeIf(item -> Objects.equals(item.getId(), user.getId()));
            data.add(user);
            return user;
        }
    }
}
  • AtomicLong의 기능을 이용해서 데이터베이스의 auto increment 기능을 구현했습니다
  • 데이터는 List로 구현했습니다. 만약 동시성 이슈로 synchronize 처리를 해야하지 않나라고 생각한다면, 테스트할 때는 싱글 스레드이므로 고려할 필요가 없습니다

그럼 FakeUserRepository를 사용해서 UserRepositoryTest를 다시 구현해보겠습니다

class UserServiceTest {

    private UserService userService;

    @BeforeEach
    void init() {
        FakeMailSender fakeMailSender = new FakeMailSender();
        FakeUserRepository fakeUserRepository = new FakeUserRepository();
        this.userService = UserService.builder()
                .uuidHolder(new TestUuidHolder("aaaaaaaaaaaaaaa"))
                .clockHolder(new TestClockHolder(1678530673958L))
                .userRepository(fakeUserRepository)
                .certificationService(new CertificationService(fakeMailSender))
                .build();
        fakeUserRepository.save(User.builder()
                .id(1L)
                .email("kok2020@naver.com")
                .nickname("kok202")
                .address("Seoul")
                .certificationCode("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
                .status(UserStatus.ACTIVE)
                .lastLoginAt(0L)
                .build());
        fakeUserRepository.save(User.builder()
                .id(2L)
                .email("kok3030@naver.com")
                .nickname("kok303")
                .address("Seoul")
                .certificationCode("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab")
                .status(UserStatus.PENDING)
                .lastLoginAt(0L)
                .build());
    }

    @Test
    void getByEmail은_ACTIVE_상태인_유저를_찾아올_수_있다() {
        // Given
        String email = "kok2020@naver.com";

        // When
        User result = userService.getByEmail(email);

        // Then
        assertThat(result.getNickname()).isEqualTo("kok202");
    }

    @Test
    void getByEmail은_PENDING_상태인_유저를_찾아올_수_없다() {
        // Given
        String email = "kok303@naver.com";

        // When
        // Then
        assertThatThrownBy(() -> {
            User result = userService.getByEmail(email);
        }).isInstanceOf(ResourceNotFoundException.class);
    }

    @Test
    void getById은_ACTIVE_상태인_유저를_찾아올_수_있다() {
        // Given
        // When
        User result = userService.getById(1);

        // Then
        assertThat(result.getNickname()).isEqualTo("kok202");
    }

    @Test
    void getById은_PENDING_상태인_유저를_찾아올_수_없다() {
        // Given
        // When
        // Then
        assertThatThrownBy(() -> {
            User result = userService.getById(2);
        }).isInstanceOf(ResourceNotFoundException.class);
    }

    @Test
    void userCreateDto를_이용하여_유저를_생성할_수_있다() {
        // Given
        UserCreate userCreate = UserCreate.builder()
                .email("kok2020@kakao.com")
                .address("Gyeongi")
                .nickname("kok202-k")
                .build();

        // When
        User result = userService.create(userCreate);

        // Then
        assertThat(result.getId()).isNotNull();
        assertThat(result.getStatus()).isEqualTo(UserStatus.PENDING);
        assertThat(result.getCertificationCode()).isEqualTo("aaaaaaaaaaaaaaa");
    }

    @Test
    void userUpdateDto_를_이용하여_유저를_수정할_수_있다() {
        // Given
        UserUpdate userUpdate = UserUpdate.builder()
                .address("Incheon")
                .nickname("kok202-n")
                .build();

        // When
        userService.update(1, userUpdate);

        // Then
        User user = userService.getById(1);
        assertThat(user.getId()).isNotNull();
        assertThat(user.getAddress()).isEqualTo("Incheon");
        assertThat(user.getNickname()).isEqualTo("kok202-n");
    }

    @Test
    void user를_로그인_시키면_마지막_로그인_시간이_변경된다() {
        // Given
        // When
        userService.login(1);

        // Then
        User user = userService.getById(1);
        assertThat(user.getLastLoginAt()).isGreaterThan(0L);
        assertThat(user.getLastLoginAt()).isEqualTo(1678530673958L);
    }

    @Test
    void PENDING_상태의_사용자는_인증_코드로_ACTIVE_시킬_수_있다() {
        // Given
        // When
        userService.verifyEmail(2, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab");

        // Then
        User user = userService.getById(2);
        assertThat(user.getStatus()).isEqualTo(UserStatus.ACTIVE);
    }

    @Test
    void PENDING_상태의_사용자는_잘못된_인증_코드를_받으면_에러를_던진다() {
        // Given
        // When
        // Then
        assertThatThrownBy(() -> {
            userService.verifyEmail(2, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaad");
        }).isInstanceOf(CertificationCodeNotMatchedException.class);
    }

}
  • 보다시피 해당 테스트에는 @SpringBootTest를 사용하지 않았습니다, 당연히 h2 데이터베이스도 사용하지 않고 있는 상태입니다. 우리가 원한 소형 테스트의 조건을 충족하고 있습니다
  • @BeforEach를 이용해서 테스트에서 사용할 데이터를 미리 만들어준 모습입니다
        FakeMailSender fakeMailSender = new FakeMailSender();
        FakeUserRepository fakeUserRepository = new FakeUserRepository();
        this.userService = UserService.builder()
                .uuidHolder(new TestUuidHolder("aaaaaaaaaaaaaaa"))
                .clockHolder(new TestClockHolder(1678530673958L))
                .userRepository(fakeUserRepository)
                .certificationService(new CertificationService(fakeMailSender))
                .build();
  • 이 부분을 보면 외부 기능을 모두 인터페이스를 사용해서 의존성을 분리해줬기 때문에 테스트에서 사용할 fake 객체로 모두 대체해서 우리가 원하는 값을 고정시킬 수 있었습니다
  • 덕분에 본래 의존된 상태에서는 값을 알 수가 없어서 테스트할 수 없었던 부분들도 명확하게 확인할 수 있게되었습니다
  • 테스트 과정은 보다시피 크게 변한게 없습니다. 의존성을 맺고 있던 객체들을 모두 fake객체로 바꿔주었고, 오로지 UserService의 로직에만 집중해서 검증하는 소형 테스트를 완성한 것입니다
  • 그리고 이 테스트를 실행해보면 중형 테스트와는 전혀 다른 속도로 테스트가 완료되는 것을 알 수 있습니다
profile
Always be happy 😀

0개의 댓글