

토비님의 강의를 보면 테스트 코드가 클린코드 책에서 제일 중요한 부분이라고 말씀하신다.
헥사고날도 클린코드의 기법중 하나이므로 테스트 코드를 작성해보자.
도메인 영역 테스트
class MemberTest {
Member member;
PasswordEncoder passwordEncoder;
@BeforeEach
void setUp() {
var registerRequest = createRegisterRequest();
passwordEncoder = createPasswordEncoder();
member = Member.register(registerRequest, passwordEncoder);
}
@Test
void registerMember() {
assertThat(member.getRoles()).isNotNull();
}
@Test
void constructorNullCheck() {
var registerRequest = new MemberRegisterRequest("test01@test.com", null, null, null);
assertThatThrownBy(()->Member.register(registerRequest, passwordEncoder))
.isInstanceOf(NullPointerException.class);
}
@Test
void verifyPassword() {
assertThat(member.verifyPassword("my_password", passwordEncoder)).isTrue();
assertThat(member.verifyPassword("wrong_password", passwordEncoder)).isFalse();
}
@Test
void changeNickname() {
assertThat(member.getNickname()).isEqualTo("테스트닉");
member.changeNickname("newNickname");
assertThat(member.getNickname()).isEqualTo("newNickname");
}
@Test
void changePassword() {
member.changePassword("newPassword", passwordEncoder);
assertThat(member.verifyPassword("newPassword", passwordEncoder)).isTrue();
}
@Test
void invalidEmail() {
assertThatThrownBy(()->{
var registerRequest = new MemberRegisterRequest("illegalEmail", "테스트닉", "my_password", new HashSet<Role>());
Member.register(registerRequest, passwordEncoder);
}).isInstanceOf(IllegalArgumentException.class); // 이메일 값 객체에서 인풋 검증이 이루어짐
}
}
일단 회원 엔티티에 대해서 테스트 코드를 작성했다.
셋업함수를 만들어서 각 테스트의 실행 전에 먼저 실행되도록 만들 수 있다.
junit의 assertThat 함수를 사용해서 테스트코드를 작성하는 거야 너무 평범해서 설명할것도 못되고.
셋업함수 내부의 createRegisterRequest와 createPasswordEncoder 부분은 테스트용 유틸 클래스인 MemberFixture로 빼두었다.
public class MemberFixture {
public static MemberRegisterRequest createRegisterRequest() {
return new MemberRegisterRequest("test01@test.com", "테스트닉", "my_password", new HashSet<Role>());
}
public static PasswordEncoder createPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
이렇게 여러 테스트에서 공통적으로 쓰는 부분을 픽스처라고 한다.
토비님의 강의에서는 직접 PasswordEncoder 인터페이스를 만들어서 여기에 테스트용 구현체를 정의했는데 나는 스프링 시큐리티를 사용해서 그냥 제공되는 PasswordEncoder를 사용했다.
class EmailTest {
@Test
void equality() {
var email1 = new Email("test01@test.com");
var email2 = new Email("test01@test.com");
assertThat(email1).isEqualTo(email2);
}
}
이메일 값 객체의 경우 테스트할 부분은 거의 없다.
record의 기능이 잘 작동하는지만 체크해두자. 나중에 혹시 클래스로 바꾸거나 하면서 이런 부분을 잊을 수도 있으니까.
어플리케이션 영역 테스트
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager entityManager;
@Test
void createMember() {
Member member = Member.register(createRegisterRequest(), createPasswordEncoder());
assertThat(member.getId()).isNull();
memberRepository.save(member);
assertThat(member.getId()).isNotNull();
entityManager.flush();
}
@Test
void duplicateEmailFail() {
Member member = Member.register(createRegisterRequest(), createPasswordEncoder());
memberRepository.save(member);
Member member2 = Member.register(createRegisterRequest(), createPasswordEncoder());
assertThatThrownBy(() -> memberRepository.save(member2))
.isInstanceOf(DataIntegrityViolationException.class);
}
}
리포지토리 테스트는 단순하다. 그냥 회원이 데이터베이스에 잘 저장되는지 확인하는 테스트와 같은 이메일을 가진 회원을 중복 저장하려고 하면 예외를 던지는지만 확인하는 테스트만 만들었다.
다만 EnitityManager를 직접 다루기 때문에 JPA에 대해서 미리 수강하지 않은 사람은 flush라는게 무슨 함수인지 모를 수도 있다. 일반 코드에서는 그냥 전체 실행하면 프레임워크가 자동으로 작동시켜주는 부분을 테스트 코드에서는 수동으로 작동시켜서 잘 돌아가는지 하나하나 체크해줘야 하기 때문에 내부 함수를 잘 모르면 골치아플 수도 있다.
저장 테스트는 멤버 객체를 처음 만들면 id가 null 인데 리포지토리에 저장하면 id가 생성된다는걸 이용해서 테스트를 했다.
마지막에 flush를 해서 변경사항을 실제 DB로 적용시키는 부분을 강의에서 넣으셨는데, 아마 DB에 문제가 있다면 이부분에서 에러가 생기지 않을까.
중복 처리 부분은 멤버 클래스에서 이메일 필드에 @NaturalId가 걸려있어서 DB가 자동생성될때 유니크 키를 이메일 칼럼에 박는다. 해당 예외가 터지는 경우 스프링은 DataIntegrityViolationException을 반환한다는 걸 이용해서 테스트를 했다.
class MemberRegisterManualTest {
@Test
void registerTestStub() {
MemberRegister memberRegister = new MemberService(
new MemberRepositoryStub(), new EmailSenderStub(), MemberFixture.createPasswordEncoder()
);
Member member = memberRegister.register(MemberFixture.createRegisterRequest());
assertThat(member.getId()).isNotNull();
}
@Test
void registerTestMock() {
EmailSenderMock emailSenderMock = new EmailSenderMock();
MemberRegister memberRegister = new MemberService(
new MemberRepositoryStub(), emailSenderMock, MemberFixture.createPasswordEncoder()
);
Member member = memberRegister.register(MemberFixture.createRegisterRequest());
assertThat(member.getId()).isNotNull();
assertThat(emailSenderMock.getTo()).hasSize(1);
assertThat(emailSenderMock.getTo().getFirst()).isEqualTo(member.getEmail());
}
@Test
void registerTestMockito() {
EmailSenderMock emailSenderMock = Mockito.mock(EmailSenderMock.class);
MemberRegister memberRegister = new MemberService(
new MemberRepositoryStub(), emailSenderMock, MemberFixture.createPasswordEncoder()
);
Member member = memberRegister.register(MemberFixture.createRegisterRequest());
assertThat(member.getId()).isNotNull();
Mockito.verify(emailSenderMock).send(eq(member.getEmail()), any(), any());
}
static class MemberRepositoryStub implements MemberRepository {
@Override
public Member save(Member member) {
ReflectionTestUtils.setField(member, "id", 1L);
return member;
}
@Override
public Member findByEmail(String email) {
return null;
}
}
static class EmailSenderStub implements EmailSender {
@Override
public void send(Email email, String subject, String body) {
}
}
static class EmailSenderMock implements EmailSender {
List<Email> to = new ArrayList<>();
public List<Email> getTo() {
return to;
}
@Override
public void send(Email email, String subject, String body) {
to.add(email);
}
}
}
헥사고날 아키텍처에서는 구현체 자체를 테스트하기보단 인터페이스를 테스트하는것을 권장한다.
그래서 MemberService 구현체의 포트인 회원 등록 인터페이스의 테스트 코드를 작성했다.
사실 이 코드는 학습을 위한 코드일 뿐이고 실제로는 @SpringBootTest를 써서 훨씬 간단하게 작성했다.
학습을 위해서 회원 등록 함수를 세가지 방식을 통해서 테스트해봤다.
첫번째는 이메일 전송 stub을 써서 그냥 이메일 보내는 부분을 없앴다. 어차피 리포지토리 테스트와 비슷하게 그냥 id가 생성되었는지만 체크하는 방식으로 테스트를 만들었다.
두번째는 이메일 전송 mock을 만들어서 회원 등록이 진행될 때 해당 mock 안의 이메일 전송 함수가 작동되는지를 체크하는 부분까지 테스트에 넣었다.
세번째는 이메일 전송 mock을 Mockito 라이브러리를 써서 만들어서 테스트를 진행해봤다.
그리고 공통적으로 리포지토리 Stub을 사용했다.
하지만 실제 DB와 연결이 된 테스트용 리포지토리를 만드려면 @SpringBootTest를 쓰는 것이 좋다.


테스트가 환경에 의존하지 않고 기존 DB 설정을 반영해서 테스트를 할 수 있다.
@SpringBootTest
@Transactional
@Import(AnthiveTestConfiguration.class)
public class MemberRegisterTest {
@Autowired
private MemberRegister memberRegister;
@Test
public void register() {
Member member = memberRegister.register(MemberFixture.createRegisterRequest());
assertThat(member.getId()).isNotNull();
}
}
테스트 코드가 굉장히 간결해졌다.
@Autowired를 써서 의존성 주입을 하는건 안티패턴이라고도 불리지만, 테스트에서는 큰 문제 없다. 여기저기서 가져다 쓸게 아니라서. (아님 롬복을 testImplementation 해서 @RequiredArgsConstructor 의존성 주입을 써도 된다)
@Transactional을 써야 테스트가 끝났을 때 DB가 다시 초기화된다.
물론 이것만으로 끝은 아니다. 회원 등록할때 사용되는 이메일 전송 인터페이스와 비밀번호 암호화 인터페이스의 구현체가 필요하다.
@TestConfiguration
public class AnthiveTestConfiguration {
@Bean
public EmailSender emailSender(){
return (email, subject, body) -> System.out.println("Sending Email" + email);
}
// 이미 스프링 시큐리티에서 같은 이름으로 빈 등록해줬으므로 생략
// @Bean
// public PasswordEncoder passwordEncoder(){
// return MemberFixture.createPasswordEncoder();
// }
}
유틸 클래스로 @TestConfiguration을 붙여서 테스트용 이메일 전송 구현체와 비밀번호 암호화 구현체를 빈으로 만들어두면 @SpringBootTest에 @Import로 넣어서 해당 빈들을 등록시킬 수 있다.
강의를 보면 테스트를 record로 만들고 서비스단에서 회원 가입할 때 이메일 중복을 체크하는 등의 기능을 추가하고 코드에서 함수를 분리해서 깔끔하게 만들고 중복 이메일 테스트도 만들긴 하는데 중요한 건 아니라서 이 포스트에선 생략함.

전체 테스트 코드. resources 폴더는 테스트를 record화 했을때 넣은 부분이다.
어댑터
@WebMvcTest(MemberApi.class)
@Import(SecurityTestConfiguration.class)
@ActiveProfiles("test")
class MemberApiTest {
@MockitoBean
private MemberRegister memberRegister;
@Autowired
MockMvcTester mvcTester;
@Autowired
private ObjectMapper objectMapper;
@Test
void register() throws JsonProcessingException {
Member member = MemberFixture.createMember(1L);
when(memberRegister.register(any())).thenReturn(member);
MemberRegisterRequest request = MemberFixture.createRegisterRequest();
String requestJson = objectMapper.writeValueAsString(request);
assertThat(mvcTester.post().uri("/api/members").contentType(MediaType.APPLICATION_JSON)
.content(requestJson))
.hasStatusOk()
.bodyJson()
.extractingPath("$.memberId").asNumber().isEqualTo(1);
verify(memberRegister).register(request);
}
@Test
void registerFail() throws JsonProcessingException {
MemberRegisterRequest request = MemberFixture.createRegisterRequest("invalid email");
String json = objectMapper.writeValueAsString(request);
assertThat(mvcTester.post().uri("/api/members").contentType(MediaType.APPLICATION_JSON)
.content(json))
.hasStatus(HttpStatus.BAD_REQUEST);
}
}
API 컨트롤러 테스트.
MockMvcTester라는 객체를 사용해서 URL 요청을 받는 부분을 테스트화할 수 있다. 여기에 JSON으로 요청 DTO가 들어온다.
객체를 JSON화 하는 것은 objectMapper를 써서 해주면 된다. 다만 체크예외가 발생하므로 함수에 throws를 넣어준다.
또한 회원 가입API에서도 인풋 검증을 하도록 @Valid를 넣어주었으니 걸리면 예외가 발생하는지도 테스트해준다.
- MediaType을 springframework거로 가져와야 한다. JUnit 거로 가져오면 에러남.
- assertThat을 Assertions에서 가져와야 한다. AssertionsForClassTypes.assertThat으로 쓰면 안된다.
@TestConfiguration
@Profile("test") // 없으면 @SpringBootTest에서 중복 빈 에러 발생
public class SecurityTestConfiguration {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername(MemberFixture.TEST_EMAIL_ADDRESS)
.password(MemberFixture.TEST_PASSWORD)
.roles("USER")
.build()
);
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/members", "/member/register").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults()) // ⭐ 핵심
.formLogin(form -> form.disable()) // ⭐ 302 방지
.build();
}
}
그리고 스프링 시큐리티가 테스트에서 작동하지 않도록 해준다.
'스프링 부트로 블로그 서비스 개발하기' 카테고리의 다른 글
| 포스트 CRUD 기능 - 테스트 코드 (1) | 2025.12.06 |
|---|---|
| 포스트 CRUD 기능 - 도메인과 서비스단 (0) | 2025.12.05 |
| 회원가입 기능 - 컨트롤러단 인풋 검증 (0) | 2025.11.27 |
| 회원가입 기능 - 스프링 시큐리티 커스텀 UserDetailsService + 도커 MySQL 적용하기 (0) | 2025.11.26 |
| 회원가입 기능 - 서비스단 헥사고날화, 회원가입 API (0) | 2025.11.26 |