5.09 테스트 전략
사용자 도메인의 테스트는 도메인 모델의 무결성과 비즈니스 규칙의 정확한 구현을 보장하기 위해 체계적으로 수행되어야 합니다. 여러 계층에 걸친 포괄적인 테스트 전략을 살펴보겠습니다.
값 객체 테스트
값 객체는 도메인의 기본적인 빌딩 블록이므로, 철저한 테스트가 필요합니다.
package net.badnom.library.user.domain.user;
@DisplayName("Email 값 객체 테스트")
class EmailTest {
@Nested
@DisplayName("이메일 생성 시")
class Creation {
@Test
@DisplayName("올바른 형식의 이메일이면 성공한다")
void shouldCreateValidEmail() {
// Given
String validAddress = "user@example.com";
// When
Result<Email> result = Email.of(validAddress);
// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.getValue().toString()).isEqualTo(validAddress);
}
@ParameterizedTest
@DisplayName("잘못된 형식의 이메일이면 실패한다")
@ValueSource(strings = {
"invalid-email",
"@example.com",
"user@",
"user@.com",
"user@example."
})
void shouldFailWithInvalidEmail(String invalidAddress) {
// When
Result<Email> result = Email.of(invalidAddress);
// Then
assertThat(result.isFailure()).isTrue();
assertThat(result.getErrorCode())
.isEqualTo(ErrorCodes.EMAIL_INVALID_PATTERN);
}
}
@Nested
@DisplayName("이메일 도메인 추출 시")
class DomainExtraction {
@Test
@DisplayName("이메일에서 도메인 부분을 정확히 추출한다")
void shouldExtractDomainCorrectly() {
// Given
Email email = Email.of("user@example.com").getValue();
// When
String domain = email.getDomain();
// Then
assertThat(domain).isEqualTo("example.com");
}
}
}엔티티 테스트
엔티티 테스트는 비즈니스 규칙과 상태 변경을 검증합니다.
package net.badnom.library.user.domain.user;
@DisplayName("Member 엔티티 테스트")
class MemberTest {
@Nested
@DisplayName("회원 생성 시")
class Creation {
private Username username;
private HashedPassword password;
private Email email;
private FullName fullName;
private UniqueEmailSpecification uniqueEmailSpec;
private UniqueUsernameSpecification uniqueUsernameSpec;
@BeforeEach
void setUp() {
username = Username.of("testuser").getValue();
password = HashedPassword.of("hashedpass").getValue();
email = Email.of("test@example.com").getValue();
fullName = FullName.of("Test User").getValue();
uniqueEmailSpec = mock(UniqueEmailSpecification.class);
uniqueUsernameSpec = mock(UniqueUsernameSpecification.class);
}
@Test
@DisplayName("유효한 데이터로 생성하면 성공한다")
void shouldCreateValidMember() {
// Given
when(uniqueEmailSpec.isSatisfiedBy(email))
.thenReturn(Result.success(true));
when(uniqueUsernameSpec.isSatisfiedBy(username))
.thenReturn(Result.success(true));
// When
Result<Member> result = Member.create(
username, password, email,
uniqueEmailSpec, uniqueUsernameSpec, fullName);
// Then
assertThat(result.isSuccess()).isTrue();
Member member = result.getValue();
assertSoftly(softly -> {
softly.assertThat(member.getUsername()).isEqualTo(username);
softly.assertThat(member.getEmail()).isEqualTo(email);
softly.assertThat(member.getFullName()).isEqualTo(fullName);
softly.assertThat(member.getCurrentTier())
.isEqualTo(MembershipTier.BASIC);
softly.assertThat(member.getStatus())
.isEqualTo(UserStatus.PENDING);
});
}
}
@Nested
@DisplayName("등급 변경 시")
class TierChange {
private Member member;
@BeforeEach
void setUp() {
member = TestMemberBuilder.aDefaultMember().build();
}
@Test
@DisplayName("상위 등급으로 변경하면 성공한다")
void shouldUpgradeTier() {
// When
Result<Void> result = member.changeTier(MembershipTier.GOLD);
// Then
assertThat(result.isSuccess()).isTrue();
assertThat(member.getCurrentTier()).isEqualTo(MembershipTier.GOLD);
// 이벤트 검증
List<DomainEvent> events = member.getDomainEvents();
assertThat(events)
.hasSize(1)
.first()
.isInstanceOf(MemberTierChangedEvent.class);
}
}
}명세(Specification) 테스트
명세의 검증 로직을 테스트합니다.
package net.badnom.library.user.domain.user.specification;
@DisplayName("UniqueEmailSpecification 테스트")
class UniqueEmailSpecificationTest {
private UserRepository userRepository;
private UniqueEmailSpecification specification;
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
specification = new UniqueEmailSpecification(userRepository);
}
@Test
@DisplayName("존재하지 않는 이메일이면 성공한다")
void shouldPassForUnusedEmail() {
// Given
Email email = Email.of("unused@example.com").getValue();
when(userRepository.existsByEmail(email)).thenReturn(false);
// When
Result<Boolean> result = specification.isSatisfiedBy(email);
// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.getValue()).isTrue();
}
}애플리케이션 서비스 테스트
명령 핸들러의 동작을 테스트합니다.
package net.badnom.library.user.application.member.command;
@DisplayName("RegisterMemberCommandHandler 테스트")
class RegisterMemberCommandHandlerTest {
private UserRepository userRepository;
private PasswordHasher passwordHasher;
private RegisterMemberCommandHandler handler;
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
passwordHasher = mock(PasswordHasher.class);
handler = new RegisterMemberCommandHandler(
userRepository, passwordHasher);
}
@Test
@DisplayName("유효한 명령을 처리하여 회원을 생성한다")
void shouldHandleValidCommand() {
// Given
RegisterMemberCommand command = new RegisterMemberCommand(
Username.of("newuser").getValue(),
Password.of("Pass123!").getValue(),
Email.of("new@example.com").getValue(),
FullName.of("New User").getValue()
);
HashedPassword hashedPassword =
HashedPassword.of("hashed").getValue();
when(passwordHasher.hash(command.password()))
.thenReturn(hashedPassword);
when(userRepository.existsByEmail(command.email()))
.thenReturn(false);
// When
Result<UserId> result = handler.handle(command);
// Then
assertThat(result.isSuccess()).isTrue();
verify(userRepository).create(any(Member.class));
}
}통합 테스트
실제 컴포넌트들과의 상호작용을 테스트합니다.
package net.badnom.library.user.integration;
@SpringBootTest
class UserIntegrationTest {
@Autowired
private RegisterMemberCommandHandler registerHandler;
@Autowired
private AuthenticateUserCommandHandler authHandler;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("회원 가입 후 로그인이 가능하다")
void shouldRegisterAndAuthenticate() {
// Given
String username = "integrationtest";
String password = "Test123!";
String email = "integration@example.com";
RegisterMemberCommand registerCommand = new RegisterMemberCommand(
Username.of(username).getValue(),
Password.of(password).getValue(),
Email.of(email).getValue(),
FullName.of("Integration Test").getValue()
);
// When
Result<UserId> registerResult = registerHandler.handle(registerCommand);
assertThat(registerResult.isSuccess()).isTrue();
AuthenticateUserCommand authCommand =
new AuthenticateUserCommand(username, password);
Result<UserId> authResult = authHandler.handle(authCommand);
// Then
assertThat(authResult.isSuccess()).isTrue();
assertThat(authResult.getValue())
.isEqualTo(registerResult.getValue());
}
}테스트 지원 클래스
테스트 데이터 생성을 돕는 빌더 클래스를 구현합니다.
package net.badnom.library.user.domain.user;
public class TestMemberBuilder {
private Username username = Username.of("testuser").getValue();
private HashedPassword password =
HashedPassword.of("hashedpass").getValue();
private Email email = Email.of("test@example.com").getValue();
private FullName fullName = FullName.of("Test User").getValue();
private MembershipTier tier = MembershipTier.BASIC;
public static TestMemberBuilder aDefaultMember() {
return new TestMemberBuilder();
}
public TestMemberBuilder withUsername(String username) {
this.username = Username.of(username).getValue();
return this;
}
public TestMemberBuilder withEmail(String email) {
this.email = Email.of(email).getValue();
return this;
}
public TestMemberBuilder withTier(MembershipTier tier) {
this.tier = tier;
return this;
}
public Member build() {
return new Member(
UserId.newId(),
username,
password,
email,
fullName,
tier
);
}
}테스트 전략의 핵심 원칙:
-
계층별 테스트
- 값 객체, 엔티티, 명세, 서비스 등 각 계층에 맞는 테스트 접근을 구현합니다
- 적절한 격리 수준을 유지합니다
- 테스트 가능한 설계를 지향합니다
-
명확한 테스트 구조
- Given-When-Then 패턴을 사용합니다
- 명확한 테스트 이름과 설명을 제공합니다
- 중첩 클래스를 통한 테스트를 그룹화합니다
-
테스트 데이터 관리
- 테스트 데이터 빌더를 활용합니다
- 의미 있는 테스트 데이터를 사용합니다
- 경계 조건을 고려합니다
-
통합 테스트 전략
- 실제 환경과 유사한 설정을 구성합니다
- 주요 시나리오를 검증합니다
- 외부 의존성을 적절히 처리합니다
이러한 체계적인 테스트 전략을 통해 도메인 모델의 정확성을 보장하고, 변경에 대한 안정성을 확보할 수 있습니다.