나는 이전에 kotlin-kotest-mockK-kotlin-DSL-BDD-테스트를-작성해보자라는 포스팅을 통해 코프링(kotlin + springboot) 에서 BDD스타일로 Test를 작성해 본 경험이 있었고 현재는 Java SpringBoot로 프로젝트를 작성하고 있다..
그런데, 현재 Authentication Server 프로젝트를 진행하면서, Test를 작성하다 보니, 테스트의 목적도 명확하지 않고, 단순 지루한 코드의 작성이라는 느낌이 강하게 들어서 다시 Java Spring을 사용한 BDD 스타일 테스트 작성 법을 찾아보았다.
BDD에서는 테스트를 내러티브하게 작성하기 때문에 애플리케이션의 행동/기능/목적을 더 이해하기 쉬워진다는 장점이 존재한다.
또한 기존에는 테스트의 멱등성을 위해 mockkito를 사용해 Mockist 방식의 test를 작성하였는데, 현재 프로젝트를 애자일 하게 진행하고 있는 만큼 내부 동작이 자주 변하기 때문에 테스트의 잦은 수정이 필요한 경우가 생겨서, Classicst방식의 Test를 작성하기로 하였다.
먼저 이전 회원 가입 테스트는 다음과 같았다.
@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("회원가입 api 테스트")
public class JoinTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private AccountRepository accountRepository;
@Test
@DisplayName("회원가입 성공")
public void testJoin() throws Exception {
//given
String email = "test@example.com";
String password = "test1234";
when(accountRepository.existsByEmail(email)).thenReturn(false);
when(accountRepository.save(Account.builder().email(email).password(password).build())).thenReturn(Account.builder().email(email).password(password).build());
//when
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post("/account/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(AccountDTO.builder().email(email).password(password).build())));
//then
verify(accountRepository, times(1)).existsByEmail(email);
resultActions.andExpect(jsonPath("$.status").value("success"));
}
@Test
@DisplayName("회원가입 실패 - 이미 가입된 이메일")
public void testJoin2() throws Exception {
//given
String email = "test@example.com";
String password = "test1234";
when(accountRepository.existsByEmail(email)).thenReturn(true);
when(accountRepository.save(Account.builder().email(email).password(password).build())).thenReturn(Account.builder().email(email).password(password).build());
//when
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post("/account/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(AccountDTO.builder().email(email).password(password).build())));
//then
verify(accountRepository, times(1)).existsByEmail(email);
resultActions.andExpect(jsonPath("$.message").value("이미 가입된 이메일입니다."));
}
@Test
@DisplayName("회원가입 실패 - 이메일 형식이 아님")
public void testJoin3() throws Exception {
//given
String email = "test";
String password = "test1234";
//when
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post("/account/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(AccountDTO.builder().email(email).password(password).build())));
//then
resultActions.andExpect(jsonPath("$.message").value("이메일 형식이 올바르지 않습니다."));
}
}
결과만 본다면 이전 코드 또한 로그인에 대한 코드를 충분히 검증할 수 있고 문제가 발생하지는 않는다.
먼저 테스트 환경과 로컬 개발환경의 차이를 두기 위해서 build.gradle을 수정해야 한다.
tasks.named('test', Test) {
useJUnitPlatform()
systemProperty 'spring.profiles.active', 'test'
}
위와 같이 테스트 시에는 test프로필을 활성화 할 수 있도록 변경한다.
다음으로는 보일러 플레이트 같이 테스트 작성시 마다 반복해서 작성해야 하는 코드를 MockMvcTest라는 클래스로 작성하고 테스트 클래스가 MockMvcTest를 상속받도록 계획했다.
@SpringBootTest
@AutoConfigureMockMvc
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MockMvcTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
protected AccountRepository accountRepository;
@Autowired
protected AccessTokenRepository accessTokenRepository;
@Autowired
protected RefreshTokenRepository refreshTokenRepository;
@Autowired
protected OauthPlatformRepository oauthPlatformRepository;
@Autowired
protected BCryptPasswordEncoder passwordEncoder;
@Autowired
protected JwtService jwtService;다
static Long accoundId = 0L;
public String getTestEmail() {
accoundId++;
return "test" + accoundId + "@email.com";
}
public String getTestPassword() {
return RandomStringUtils.random(10, true, true)+accoundId;
}
}
getTestEmail()
은 테스트의 멱등성을 위해 매번 다른 AccountEmail을 생성하기 위해 작성하였으며, getTestPassword()
의 경우는 마찬가지로 매번 다른 Password를 랜덤하게 가져오기 위해 작성하였다. 왠만하면 RandomStringUtils만으로도 수많은 경우의 수가 존재하기 때문에 겹칠 확률이 적긴 하지만, 혹시 모르는 상황을 대비하여 accountId를 더해주므로서 유니크를 보장했다.
@DisplayName("회원가입 테스트")
public class JoinTest extends MockMvcTest {
@Nested
@DisplayName("클라이언트가 회원가입을 요청할때")
class Describe_account_join {
String email;
String password;
String duplicatedEmail;
@BeforeEach
void setUp() {
email = getTestEmail();
password = getTestPassword();
duplicatedEmail = getTestEmail();
LocalAccount account = LocalAccount.builder()
.email(duplicatedEmail)
.password(password)
.build();
accountRepository.save(account);
}
@Nested
@DisplayName("정상적인 요청이라면")
class Context_WhenRequestIsValid {
@Test
@DisplayName("회원가입에 성공한다")
void it_success_join() throws Exception {
ResultActions result = mockMvc.perform(MockMvcRequestBuilders.post("/account/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(Map.of("email", email, "password", password))));
result.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("success"));
}
}
@Nested
@DisplayName("이메일 중복이라면")
class Context_WhenEmailIsDuplicated {
@DisplayName("이메일 중복 에러를 반환한다")
@Test
void it_returns_email_duplicated_error() throws Exception {
ResultActions result = mockMvc.perform(MockMvcRequestBuilders.post("/account/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(Map.of("email", duplicatedEmail, "password", password))));
result.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("이미 가입된 이메일입니다."));
}
}
@Nested
@DisplayName("요청이 비정상적이면 - 이메일 형식이 아님")
class Context_WhenRequestIsInvalid {
String invalidEmail = "invalidEmail";
@Test
@DisplayName("이메일 형식 에러를 반환한다")
void it_returns_email_format_error() throws Exception {
ResultActions result = mockMvc.perform(MockMvcRequestBuilders.post("/account/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(Map.of("email", invalidEmail, "password", password))));
result.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("이메일 형식이 올바르지 않습니다."));
}
}
}
}
먼저 MockMvcTest를 상속받는 것을 선언하고 BDD스타일을 구현하기 위해 @Nested 어노테이션을 사용하여 내부 클래스를 작성한다.
또한 Classicist 방식을 사용하면서 테스트의 멱등성을 위해 @BeforeEach를 사용하여 db에 테스트에 필요한 account계정을 저장해 두었다.
회원가입이라는 큰 범주 안에 행동 - 조건 - 검증 이 분비 별로 잘 나타나는 것을 확인 할 수 있다.