기존의 테스트 방식인 TDD에서 개발자가 좀 더 이해하기 쉽고 효율적으로 사용하기 위해 기능을 추가한 것이 BDD이다.
Behavior Driven Development의 약자로 TDD에서 따왔기 때문에 기존 TDD와 추구하는 의미는 비슷하다.
BDD는 행동을 기반하여 TDD를 수행하자는 공통의 이해인데, 이를 한 문장으로 하면 다음과 같다.
"행동에 기반하여 TDD를 수행하자는 공통의 이해"를 의미한다.
Given
- 테스트를 위해 주어진 상태
- 테스트 대상에게 주어진 조건
- 테스트가 동작하기 위해 주어진 환경
When
- 테스트 대상에게 가해진 어떠한 상태
- 테스트 대상에게 주어진 어떠한 조건
- 테스트 대상의 상태를 변경시키기 위한 환경
Then
- 앞선 과정의 결과
즉, 어떤 상태에서 출발(given)하여 어떤 상태이 변화를 가했을 때(when) 기대하는 어떠한 상태가 되어야 한다.(then)
예시
public class MaxNum{
public int Max(int a, int b){
return a>b?a:b;
}
}
public class MaxNumTest{
MaxNum maxnum = new MaxNum();
@Test
void max(){
//given
int a = 3;
int b = 4;
//when
int result = maxnum.Max(a,b);
//then
assertEquals(result, b);
}
}
그렇다면 이번에는 Controller에서 Test를 통해 알아보자
실습을 통해 만들어 본 코드를 가지고 설명을 해보도록 하겠다. Test 코드에 대한 내용만 설명할 것으로 연관되는 다른 클래스들에 대한 자세한 내용은 해당 링크에서 확인이 가능하다. 실습 코드
현재 코드는 아이디를 중복검사를 하는 코드로 Test 종류에는 회원가입 성공, 회원가입 실패의 2가지 경우의 Test가 존재한다.
@WebMvcTest
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName("회원가입 성공")
void join_success() throws Exception {
// given
UserJoinRequest userJoinRequest = UserJoinRequest.builder()
.userName("han")
.password("1q2w3e4r")
.email("oceanfog1@gmail.com")
.build();
User user = userJoinRequest.toEntity();
UserDto userDto = UserDto.fromEntity(user);
when(userService.join(any())).thenReturn(userDto);
// when, then
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON) // Json 타입으로 사용
.content(objectMapper.writeValueAsBytes(userJoinRequest))) // 삽입한 데이터 dto를 json 형식으로 변환
.andDo(print())
// userName 존재 여부 확인
.andExpect(jsonPath("$..userName").exists())
// userName의 값 비교
.andExpect(jsonPath("$..userName").value("han"))
.andExpect(status().isOk());
}
@Test
@DisplayName("회원가입 실패")
void join_fail() throws Exception {
UserJoinRequest userJoinRequest = UserJoinRequest.builder()
.userName("han")
.password("1q2w3e4r")
.email("oceanfog1@gmail.com")
.build();
// 이전에는 when/thenReturn을 통해 구현했는데 그렇게 하면 given 구역에서 when을 사용하면 헷갈릴 수 있으므로 given으로 구역을 표시하며 정확히 한다.
given(userService.join(any()))
.willThrow(new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, ""));
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userJoinRequest)))
.andDo(print())
.andExpect(status().isConflict());
}
}
@WebMvcTest(xxx.class)
@Autowired
MockMvc mockMvc; // MockMvc를 의존함
@MockBean // @Autowired아님 --> HospitalService는 테스트를 위해 가짜 객체를 쓰겠다는 뜻
UserService userService; // 가짜 객체를 쓰면 좋은점 DB와 상관없이 테스트 가능
MockMvc의 코드는 모두 합쳐져 있어 구분하기는 애매하지만 전체적인 'When-Then'의 구조를 갖추고 있다.
MockBean은 실제 빈 개체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행한다.
@MockBean이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다. 그렇기에 해당 객체는 개발자가 Mockito의 given( )메서드를 통해 동작을 정의해야 한다.
❓ ObjectMapper란?
JSON 컨텐츠를 Java 객체로 deserialization 하거나 Java 객체를 JSON으로 serialization 할 때 사용하는 Jackson 라이브러리의 클래스이다.
ObjectMapper는 생성 비용이 비싸기 때문에 bean/static으로 처리하는 것이 좋다.
Jackson 라이브러리에 관한 내용은 더 공부하고 나중에 따로 작성해보도록 하겠다.
when(userService.join(any())).thenReturn(userDto);
-------
userService.join - Mocking할 메서드(가짜 객체 지정)
any() - 메서드의 파라미터(어떤 값이 와도 상관없다는뜻)
any(String.class) - String에 대한 어떤 값이 와도 상관없다는 뜻
eq() : any는 모든 값을 허용하므로 만약 특정 값을 입력받고 싶을때는 eq()를 사용함
.thenReturn(userDto); - 해당 메서드가 반환하는 값
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON) // Json 타입으로 사용
.content(objectMapper.writeValueAsBytes(userJoinRequest))) // 삽입한 데이터 dto를 json 형식으로 변환
.andDo(print())
// userName 존재 여부 확인
.andExpect(jsonPath("$..userName").exists())
// userName의 값 비교
.andExpect(jsonPath("$..userName").value("han"))
.andExpect(status().isOk());
회원가입 성공에서는
when(userService.join(any())).thenReturn(userDto);
회원가입 실패에서는
given(userService.join(any()))
.willThrow(new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, ""));
를 사용했다
왜 다르게 사용을 했을까❓
그 이유는 처음에 말했던것과 같이 TDD에서 개발자가 이해하기 쉽게 만들기 위해서 BDD를 사용한다고 말했었는데 현재 Given구역 안에는 Mocking할 메서드, 즉 가짜 객체를 선언해주는데 when으로 사용하면 when구역과 헷갈릴 수 있으므로 Given을 통해 구역을 정확히 구분하여 사용한 것이다.
즉, 회원가입 성공에서 사용한 when은 TDD이고 실패에서 사용한 given은 BDD이다.
import문을 살펴봐도 when은 Mockito을 통해 추가가 되어 있고 given은 BDDMockito을 통해 추가가 되어있다.