이 시리즈는 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
의 로직에만 집중해서 검증하는 소형 테스트
를 완성한 것입니다