본문 바로가기
💊 Java/--- SpringBoot

[Spring Boot] JUnit5 BDDMockito로 알아보는 TDD와 BDD 의 차이 및 BDD 실습

by Wonit 2021. 4. 22.

목차

  • TDD 란?
    • TDD 란?
    • TDD 예제
  • BDD 란?
    • BDD 란?
    • BDD 예제
  • TDD vs BDD
    • TDD와 BDD의 한 표 요약
  • BDDMockito를 이용한 BDD
    • 실습 환경 구성
    • Mockito vs BDD Mockito
    • Stubbing
    • given
      • any()
      • anyString()
      • anyLong()
      • willReturn()
      • will()
      • willThrow()

 

본 주제를 학습하기 위해서는 Java와 JUnit5와 BDDMockito 라이브러리의 의존성이 필요합니다.

 


소프트웨어를 개발하면서 지금까지 느낀 가장 큰 변화가 나에게는 TDD가 아닐까 싶다.

우연한 기회에 TDD를 스스로 도입하였을 때, 내가 들었던 첫 생각은

너무 귀찮은데,... 이렇게 하다가 언제 끝내지...? 이거 그냥 테스트 안 짜고 바로 할 수 있는데..

이런 생각을 갖고 TDD를 증오 하던 시간이 얼마 지나지 않아서 실타래처럼 꼬인 아주 모호한 기능(Spring Security 에서) 구현해야할 때가 있었다.


늘 그렇듯 TDD로 실패하는 코드를 먼저 짜고 내가 뭘 해야할지를 적어가면서 하나씩 테스트를 수행하고 기능을 고치면서 점점 실타래가 풀리는 것 같은 기분이 들게 되었다.

 

그리고 하나 하나 기능을 추가하며 최고로 복잡하던 그 기능을 하나씩 풀어가며 느꼈던 감정은 증오에서 맹신으로 바뀌게 된 계기 같았다.

 

이렇듯 TDD는 여러 개발자에게 좋은 개발 습관을 갖게 할 수 있는 방법 중 단연 최고라고 생각한다.


오늘은 TDD에서 조금 더 진화한 BDD에 대해서 알아보려 한다.

 

그 전에 TDD와 BDD에 대한 용어 정리를 해보자.

 

TDD, Test Driven Development

 

TDD는 Test Driven Development으로 테스트가 개발을 주도한다는 개념으로 사용된다.

 

TDD는 테스트를 먼저 만들고 테스트를 통과하기 위한 행동들이 모두 개발을 주도하는 것을 목표로 한다.


보통 테스트는 개발이 끝난 후에 하는 과정이라고 생각할 수 있다.


하지만 TDD는 개발 중간 중간에 끊임 없는 테스트를 통해 기능을 개발해야 한다.

 

TDD는 Kent Beck이 고안해낸 방법론으로 이 책은 하나의 바이블로 여러 개발자들에게 주목받고 있는듯 하다.

 

TDD를 번역한 한글판 책이 있는데, 혹시 이에 대한 정보가 궁금하다면 해당 블로그에 있는 개발자 책 읽기테스트 주도 개발 - Kent Beck 을 확인해 보는 것도 좋은 방법인 것 같다.

TDD 예제

 

계산기를 TDD로 구현해보자면 다음과 같다.

 

Step 1. 우선 비어있는 Calculator 클래스와 CalculatorTest 클래스를 생성하자.

public class Calculator {

}

public class CalculatorTest {
    Calculaotr calc = new Calculator();
}

 

간단하게 테스트 코드에 Calculator 클래스의 인스턴스를 생성해주자.

 

Step 2. 내가 원하는 기능(덧셈, plus())이 어떻게 생겼고 뭘 반환하는지 CalculatorTest 에 작성한다.

public class Calculator {

}

public class CalculatorTest {
    Calculaotr calc = new Calculator();

    @Test
    void plus() {
        int a = 10;
        int b = 20;
        int result = calc.plus(a, b);
    }

}

 

현재 Calculator 클래스에는 plus라는 메서드가 존재하지 않기 때문에 실패하게 된다.

 

Step 3. Calculator 클래스에 해당 메서드를 통과할 수 있는 간단한 클래스를 구현한다.

public class Calculator {
    public int plus(int a, int b) {
        return 0;
    };
}

public class CalculatorTest {
    Calculaotr calc = new Calculator();

    @Test
    void plus() {
        int a = 10;
        int b = 20;
        int result = calc.plus(a, b);
    }
}

 

Calculator의 plus 메서드에서는 어떤 값이 반환되도 좋다.


그냥 일단 컴파일을 성공시키는 것이 목적이다.

 

Step 4. Calculator 클래스에서 어떤 결과를 내어야 할지 CalculatorTest 클래스에서 다시 정의한다.

public class Calculator {
    public int plus(int a, int b) {
        return 0;
    };
}

public class CalculatorTest {
    Calculaotr calc = new Calculator();

    @Test
    void plus() {
        int a = 10;
        int b = 20;
        int result = calc.plus(a, b);

        assertEquals(result, a + b);
    }
}

assertEquals 는 JUnit 5에 존재하는 검증문으로 매개변수 2개가 서로 동일하면 테스트 성공을 의미한다.

 

현재 result 에서는 0을 반환하니 해당 테스트는 또 실패할 것이다.

 

Step 5. Calculator 클래스에서 해당 테스트를 통과할 수 있는 로직을 구현한다.

public class Calculator {
    public int plus(int a, int b) {
        return a + b;
    };
}

public class CalculatorTest {
    Calculaotr calc = new Calculator();

    @Test
    void plus() {
        int a = 10;
        int b = 20;
        int result = calc.plus(a, b);

        assertEquals(result, a + b);
    }
}

BDD, Behavior Driven Development

 

BDD는 전혀 새로운 방법이 아니다.

 

Danial Terhorst-North와 Charis Matts가 착안 BDD Introducing - Dan north & associates안 방법론으로 BDD의 모든 근간은 TDD에서 착안되었기 때문에 TDD가 추구하는 가치와 크게 다르지 않다.

 

 

Danial Terhorst-North가 처음으로 BDD를 생각해 낸 때는 바로 TDD를 수행하고 있던 도중이었다고 한다.

 

The deeper I got into TDD, the more I felt that my own journey had been less of a wax-on, wax-off process of gradual mastery than a series of blind alleys. I remember thinking “If only someone had told me that!” far more often than I thought “Wow, a door has opened.” I decided it must be possible to present TDD in a way that gets straight to the good stuff and avoids all the pitfalls.

My response is behaviour-driven development (BDD). It has evolved out of established agile practices and is designed to make them more accessible and effective for teams new to agile software delivery. Over time, BDD has grown to encompass the wider picture of agile analysis and automated acceptance testing

 

이 말을 간단하게 하자면 TDD를 하다가 해당 코드를 분석하기 위해서 많은 코드들을 분석해야하고 복잡성으로 인해 '누군가가 나에게 이 코드는 어떤식으로 짜여졌어! 라고 말을 해줬으면 좋았을 텐데' 라는 생각을 하다가 보니 행동 중심 개발을 하면 좋겠다고 생각했다.

 

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

 

BDD는 애플리케이션이 어떻게 행동해야 하는지에 대한 공통된 이해를 구성하는 방법이다.

BDD의 행동

 

BDD 에서는 행동에 대한 스펙은 다음과 같다.

 

  1. Narrative
  2. Given / When / Then

모든 테스트 문장은 Narrative 하게 되어야 한다. 즉, 코드보다 인간의 언어와 유사하게 구성되어야 한다.

 

TDD 에서는 사실상 테스트 코드를 이용한 구현에 초점이 맞춰저 있다.

 

하지만 BDD는 그렇지 않다.

 

BDD는 TDD를 수행하려는 어떠한 행동과 기능을 개발자가 더 이해하기 쉽게하는 것이 목적이다.

 

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

 

위에서 말 한 Narrative를 수행하기 위한 실질적인 개념이라고 생각하면 될 것 같다.

 

  • Given
    • 테스트를 위해 주어진 상태
    • 테스트 대상에게 주어진 조건
    • 테스트가 동작하기 위해 주어진 환경
  • When
    • 테스트 대상에게 가해진 어떠한 상태
    • 테스트 대상에게 주어진 어떠한 조건
    • 테스트 대상의 상태를 변경시키기 위한 환경
  • Then
    • 앞선 과정의 결과

즉, 어떤 상태에서 출발 (given) 하여 어떤 상태의 변화를 가했을 때 (when) 기대하는 어떠한 상태가 되어야 한다.(then)

BDD 예제

 

앞선 상황에서 TDD의 흐름을 빼고 BDD의 핵심 개념인 행동에 따른 Given When Then 을 도입하면 다음과 같다.

 

public class Calculator {
    public int plus(int a, int b) {
        return a + b;
    };
}

public class CalculatorTest {
    Calculaotr calc = new Calculator();

    @Test
    void plus() {
        // given
        int a = 10;
        int b = 20;

        // when
        int result = calc.plus(a, b);

        // then
        assertEquals(result, a + b);
    }
}

 

큰 차이가 없지 않나?

 

당연한 것이다. BDD 자체가 TDD에서 더 새로운 개념이 아니라 TDD를 더 잘, 더 멋지게, 더 협조적으로 사용하기 위한 방법이기 때문이다.

TDD vs BDD

앞서 말 했지만 TDD와 BDD를 비교한다는 것은 약간 다른 주제의 이야기라 비교 한다는 것이 힘들것 같지만 개인적으로 비교를 해보자면 다음과 같은 표가 나올 것 같다.

 

이름 창시자 Based 핵심 라이브러리
TDD Kent-Beck 시나리오 기반 테스트가 주도하는 개발 JUnit5, Mockito
BDD Dan North 행동 기반 자연어와 더 가깝게 TDD JUnit5, BDDMockito

BDDMockito를 이용한 BDD

BDD를 실현하기 위해서는 Given / When / Then 구조를 잘 사용해야 한다.

 

앞선 방법처럼 주석을 이용하여 구분하는 것도 BDD이지만 이보다 더 BDD 스럽게 BDD를 실천할 수 있는 방법이 바로 BDDMockito를 사용하는 것이다.

 

BDDMockito란 Mockito 라이브러리 내에 존재하는 BDDMockito 클래스를 말 하며 BDD 지향적인 개발을 mockito 에서 api 형태로 제공한다.

실습 환경 구성, Dependencies

 

Springboot 프로젝트를 생성하면 자동으로 JUnit-jupiter 의존성이 추가되는데 해당 의존성이 추가되며 함께 mockito 라이브러리가 추가된다.

 

 

maven

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
    <scope>test</scope>
</dependency>

 

gradle

testImplementation group: 'org.mockito', name: 'mockito-all', version: '1.10.19'

Mockito vs BDDMockito

순수 Mockito에서 BDD의 Given / When / Then 을 위해서 다음과 같이 when(obj) 메서드와 thenReturn() 이용하고 verity() 구문을 이용해 검증한다.

 

이런식으로 특정 상황에 대한 (when과 같이) 우리가 가짜로 결과를 만들어 주는 것을 Stubbing(스터빙) 이라고 한다. 즉 가짜로 수행할 객체를 넣어주는 것이다.

 

when(phoneBookRepository.contains(momContactName))
  .thenReturn(false);

phoneBookService.register(momContactName, momPhoneNumber);

verify(phoneBookRepository)
  .insert(momContactName, momPhoneNumber);

 

이게 일련의 BDD의 Stubbing 과정인데, 뭔가 맞지 않는 부분이 있다.

 

  • when()
  • thenReturn()
  • verity()

 

위에 존재하는 3가지 행동 과정인데, 뭔가 이상하다.

 

개념적으로 given 에 해당되는 Mockito의 when(phoneBookRepository.contains(momContactName))의 이름이 when이라 쉽게 햇갈릴 수 있게 되어있다.

 

이러한 문제점을 BDDMockito의 given() 메서드를 이용해서 해결할 수 있다.

BBDMockito를 이용하면 Mockito의 when()given() 이라는 메서드로 더 정확한 의미 전달을 할 수 있다.

 

given(someClass.method()).willReturn() 

 

 

given() 메서드

우선 given() 메서드는 BDD의 givne을 그대로 내포하고 있다.

 

이런 givne()의 파라미터를 이용해서 어떤 상황에 즉, 어떤 메서드가 실행되었을 때의 테스트를 위한 상황을 설정할 수 있다.

 

다음과 같은 상황의 메서드가 있다고 가정해보자.

 

public interface MemberRepository extends JpaRepository<Member, Long> {
    boolean existsByEmail(String email);
}

public class MemberService {
    @Autowired
    private MemberRepository memberRepository;

    public boolean isExistEmail(String email) {
        return memberRepository.existsByEmail(email); // 존재하면 true, 존재하지 않으면 false 반환
    }
}

 

그럼 아래와 같이 테스트 코드를 구성할 수 있다.

 

public interface MemberRepository extends JpaRepository<Member, Long> {
    boolean existsByEmail(String email);
}

public class MemberService {
    @Autowired
    private MemberRepository memberRepository;

    public boolean isExistEmail(String email) {
        return memberRepository.existsByEmail(email); // 존재하면 true, 존재하지 않으면 false 반환
    }
}

public class MemberServiceTest {
    @MockBean
    private MemberService memberService;

    private final MemberRepository memberRepository = mock(MemberRepository.class);

    @BeforeEach
    void stubbing() { 
        given(memberRepository.existsByEmail(any(String.class)))
                    .willReturn(false);
    }
}

 

그럼 더 자세히 알아보자.

 

 

위의 Stubbing 문장에서 3가지의 의미를 담고 있다.

 

  1. Mocking 할 메서드
  2. 해당 메서드의 파라미터
  3. 해당 메서드를 수행했을 때 반환하는 값

Mocking 할 메서드

 

Mocking을 할 메서드는 뭘까?

 

우리는 지금 MemberService를 테스트를 하려한다.

 

Unit Test에서 중요한 것은 테스트하려는 대상의 고립이다.

 

테스트 대상을 고립한다는 뜻은, 테스트 대상에 연관된 다른 객체들은 관여하지 않도록 우리가 가짜 객체를 넣어줘야 한다는 소리와 비슷하다고 할 수 있다.

 

이렇게 테스트 대상을 고립하기 위해서 Mockito의 mock() 를 이용하였고, 테스트 대상이 특정 결과를 수행하기 위해 연관된 객체의 연산을 주입해주면 된다.

 

MemberService 에는 현재 isExistByEmail 메서드가 존재하고 해당 메서드 내부에 MemberRepository 인스턴스가 existsByEmail 연산을 수행하고 있으므로 우리는 MemberRepository의 existsByEmail 을 가짜로 주입해주면 된다.

 

해당 메서드의 파라미터

 

해당 메서드, 즉 existsByEmail 이라는 메서드는 String 으로 email을 받고 있다.

 

그럼 우리에게 여러 선택지가 존재한다.

 

  • 모든 값을 받았을 때의 행동 정의하기
  • 특정 값을 받았을 때의 행동 정의하기

모든 값을 받았을 때의 행동 정의하기, any()

 

모든 객체가 들어왔을 때의 행동은 any(Object object) 를 이용할 수 있다.

 

여기서는 String 인 모든 객체가 가능하다는 소리다.

 

위와 같은 상황에 (어떠한 String 값이라도 가능한 상황) 사용할 수 있는 메서드가 여럿 존재한다

 

  • anyString() : 어떠한 문자열
  • anyLong() : 어떠한 Long
  • anyBoolean() : 어떠한 Boolean 값
  • any() : 모든 객체
public class MemberServiceTest {
    // ... 생략
    @BeforeEach
    void stubbing() { 
        given(memberRepository.existsByEmail(anyString()));

        given(memberRepository.findById(anyLong()));

        given(memberRepository.findAllByOffline(anyBoolean()))
                    .willReturn(false);
    }
}

특정 값을 받았을 때의 행동 정의하기, eq()

위의 상황에서는 모든 String을 받았을 때의 행동을 정의하는데, 만약 특정한 String 값이 들어와야 한다면?


혹은 특정한 객체가 들어와야 한다면?


그럴 때 사용하는게 바로 eq() 메서드이다.

 

  • eq() : 동일한 값 혹은 객체의 행동

해당 메서드를 수행했을 때 반환하는 값

 

위와 같이 다양한 방법으로 행동을 정의하였다.


그럼 이제 해당 행동에 적절한 반환을 해줘야 한다.


행동을 반환할 때는 크게 3가지 방법이 존재한다.

  • willReturn()
  • will()
  • willThrow()

willReturn()

뜻 그대로 반환 값을 명시해줄 수 있다.

public class MemberServiceTest {

    // ... 생략
    @BeforeEach
    void stubbing() { 
        given(memberRepository.existsByEmail(any(String.class)))
                    .willReturn(false);
    }

    @Test
    void exists_valid() {
        boolean isExist = memberService.isExistEmail("valid@gmail.com");

        assertTrue(isExist);
    }
}

will() + invocation

will()은 willReturn과는 조금 다르다.

 

willReturn은 고정된 값을 반환하는데, will() 에서는 invocation 을 통해서 새로운 객체를 반환하거나 아예 새로운 행동을 반환할 수 있다.

 

public class MemberServiceTest {

    // ... 생략
    @BeforeEach
    void stubbing() { 
        given(memberRepository.findById(anyLong()))
                    .will(invocation -> {
                        Member member = invocatoin.getArgument(0);
                        return MemberData.builder()
                                    .email("test123@gmail.com")
                                    .isFound(true)
                                    .build();
                    });
    }

    @Test
    void exists_valid() {
        MemberData memberData = memberService.findMember(1L);

        assertNotNull(memberData);
    }
}

willThrow()

 

willThrow는 말 그대로 예외를 던진다.

 

public class MemberServiceTest {

    @BeforeEach
    void stubbing() { 
        given(memberRepository.existsByEmail(eq("hello@gmail.com")))
                    .willThrow(MemberDuplicationException("exists");
    }

    @Test
    void exists_valid() {
        MemberDuplicationException exeption = assertThrow( 
            MemberDuplicationException.class,
            () -> memberService.isExistEmail("valid@gmail.com")
        );

        assertEquals(exception.getMessage(), "exists");
    }
}

 

댓글0