TDD/BDD

배지원·2022년 12월 5일
0

SpringBoot

목록 보기
11/11

정의

기존의 테스트 방식인 TDD에서 개발자가 좀 더 이해하기 쉽고 효율적으로 사용하기 위해 기능을 추가한 것이 BDD이다.

Behavior Driven Development의 약자로 TDD에서 따왔기 때문에 기존 TDD와 추구하는 의미는 비슷하다.

BDD는 행동을 기반하여 TDD를 수행하자는 공통의 이해인데, 이를 한 문장으로 하면 다음과 같다.

"행동에 기반하여 TDD를 수행하자는 공통의 이해"를 의미한다.


구조

  • 모든 테스트 문장은 Given, When, Then으로 나눠서 작성할 수 있어야 한다.

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);
  }
}
  • 이처럼 given에는 기능이 동작하는데 기본값(변수)등 상태가 주어진다.
  • when에는 기능을 구현(메서드 실행)하는 곳으로 상태, 조건이 실행된다
  • then에는 위를 통한 결과가 출력된다.

그렇다면 이번에는 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());
    }
}

(1) @WebMvcTest(테스트 대상 클래스 이름.class)

@WebMvcTest(xxx.class)
  • 해당 클래스명을 지정을 안해주면 모든 Controller 관련 빈 객체가 모두 로드가 된다. @SpringBootTest보다 가볍게 테스트하기 위해 사용한다.

(2) @MockBean

@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 라이브러리에 관한 내용은 더 공부하고 나중에 따로 작성해보도록 하겠다.

(4) @Test

1. Given

  • 어떤 메서드가 실행되었을 때의 테스트를 위한 상황을 설정할 수 있다.
  • 유저에게 요청받는 DTO UserJoinRequest의 데이터를 넣어준다.
  • Given 메서드
when(userService.join(any())).thenReturn(userDto);
-------
userService.join - Mocking할 메서드(가짜 객체 지정)

any() - 메서드의 파라미터(어떤 값이 와도 상관없다는뜻)
any(String.class)  - String에 대한 어떤 값이 와도 상관없다는 뜻
eq() : any는 모든 값을 허용하므로 만약 특정 값을 입력받고 싶을때는 eq()를 사용함

.thenReturn(userDto); - 해당 메서드가 반환하는 값

2. when,then

mockMvc.perform(post("/api/v1/users/join")
  .contentType(MediaType.APPLICATION_JSON)        // Json 타입으로 사용
  .content(objectMapper.writeValueAsBytes(userJoinRequest)))      // 삽입한 데이터 dto를 json 형식으로 변환
  • Controller에서 Test하고 싶은 API주소를 perform을 통해 입력
  • .contentType( )을 통해 타입 지정(Json)
  • .content( )을 통해 삽입한 데이터 dto를 Json형식으로 유저에게 반환함
.andDo(print())
// userName 존재 여부 확인
.andExpect(jsonPath("$..userName").exists())
// userName의 값 비교
.andExpect(jsonPath("$..userName").value("han"))
.andExpect(status().isOk());
  • andDo(print())를 통해 Test 과정을 콘솔에 출력
  • andExpect(jsonPath("$..userName").exists()) : json형식의 결과 데이터중 userName의 데이터가 존재하는지 검사
  • .andExpect(jsonPath("$..userName").value("han")) : userName의 값이 "han"인지 검사
  • .andExpect(status().isOk()); : 현재 상태값이 정상인지 확인


(4) 결과


그런데 여기서 궁금한점이 있다. 회원가입 성공 테스트와 실패 테스트의 Given의 구조가 약간 다르다.

회원가입 성공에서는

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을 통해 추가가 되어있다.



참고 자료 : https://go-coding.tistory.com/102

profile
Web Developer

0개의 댓글