Mockito

YoungJun Kim·2022년 10월 18일
0

Application Test

목록 보기
2/6

단위 테스트와 Mock

Mock 객체는 실제 객체와 비슷하게 동작하지만 개발자가 직접 그 객체의 행동을 관리하는 객체를 의미한다. 이러한 Mock 객체를 쉽게 만들고 관리하고 검증할 수 있는 라이브러리로는 Mockito를 주로 사용한다.

JUnit은 주로 단위 테스트를 수행하기 위해 사용된다. 단위 테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위를 테스트 하는 것을 의미한다. 따라서 일반적으로는 메서드 단위로 기능의 유효성을 검증하게 되는데, 어떠한 행위를 하나의 단위로 생각할 수도 있다.

예를 들어, Spring 애플리케이션은 기본적으로 Controller - Service - Repository의 계층화 아키텍처로 구성한다. 이 때 Service의 한 메서드를 단위로 보고 각각을 테스트 할 수도 있고, 진입점인 Controller 부터 Repository 까지의 메서드들을 하나의 행위로 보아 단위 테스트를 진행할 수도 있다.

아무튼 단위 테스트를 진행하기 위해서 일반적으로 Mock 객체를 사용하게 된다. 일반적으로 객체 지향 프로그래밍은 객체 간의 협력을 통해 프로그래밍을 하기 때문에 다른 객체와의 연관 혹은 의존 관계에 의해 하나의 객체만을 단독적으로 테스트하기는 어렵다. 이럴 경우 테스트 대상이 되는 객체와 연관되는 혹은 의존되는 객체를 Mock 객체로 정의하여 Stubbing을 통해 임의로 테스트 데이터를 개발자가 직접 관리해주어야 한다.

만약 Controller 부터 Repository 까지의 과정을 하나의 행위로 판단한다면, Service, Repository는 Mock 객체로 사용하지 않고 테스트를 할 것이다. 이외 나머지는 외부의 환경을 연동하지 못하는 경우(S3와 연동하여 파일을 업로드 하는 객체 등)에는 해당 객체를 Mock 객체로 만들어 Stubbing을 해야 할 것이고, 각각의 메서드 하나를 단위로 구분할 경우에는 Controller Test를 하는 경우에 Service를 Mock 객체로 만들어 Stubbing을 해야 할 것이다.


Mockito로 Mock 객체 생성 및 주입하기

예제 시나리오

간단하게 테스트를 위해 시나리오 하나를 만들어보자.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private Integer age;

    private MemberLevel memberLevel;

    public void updateLevel(MemberLevel memberLevel) {
        this.memberLevel = memberLevel;
    }

}

public enum MemberLevel {

    JUNIOR,
    SENIOR,
    MASTER

}

우선 Member 엔티티 하나를 만들어 주었다. memberLevel이라는 필드를 수정하는 updateLevel 이라는 메서드를 가지고 있다.

public interface MemberRepository extends JpaRepository<Member, Long> {}

public interface LevelCalculator {

    MemberLevel calculateMemberLevel(int age);
    
}


public class MemberService {

    public MemberRepository memberRepository;

    public LevelCalculator levelCalculator;
    
    public MemberService(MemberRepository memberRepository, LevelCalculator levelCalculator) {
        this.memberRepository = memberRepository;
        this.levelCalculator = levelCalculator;
    }

    public MemberLevel updateLevel(Long memberId) {
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -> 
                        new IllegalArgumentException("ID " + memberId + " Member Not Found"));

        MemberLevel memberLevel = levelCalculator.calculateMemberLevel(member.getAge());

        member.updateLevel(memberLevel);

        return memberLevel;
    }

}

그리고 Member 엔티티와 관련하여 DB에 접근하는 MemberRepository, age 값에 따라 Level을 반환시켜주는 LevelCalculator, 실제 비즈니스 로직을 담당하는 MemberService 클래스들을 각각 정의했다.

이제 MemberService의 updateLevel 메서드를 테스트해보자.

Mock 객체 생성 및 주입

updateLevel 메서드를 테스트 하기 위해서는 당연히 MemberService 객체가 필요하다. 그런데 MemberService는 각각 LevelCalculator, MemberRepository 인터페이스에 의존하고 있다. 따라서 MemberService 객체를 생성하기 위해서는 LevelCalculator, MemberRepository의 구현체를 주입받아야 할 것이다.

그런데 만약 아직까지 구현체를 정의하지 않았다고 했을 경우, 연관 관계를 주입할 수 없기 때문에 MemberService를 테스트 할 수 없다. 이럴 경우에는 Mock 객체를 사용하는 것을 고려할 수 있다.

@Mock

@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {

    MemberService memberService;

    @Mock
    MemberRepository memberRepository;

    @Mock
    LevelCalculator levelCalculator;

    @BeforeEach
    void init() {
        memberService = new MemberService(memberRepository, levelCalculator);
    }

    @Test
    void memberServiceTest() {
        Long stubbedMemberId = 10L;
        MemberLevel memberLevel = memberService.updateLevel(stubbedMemberId);

        assertThat(memberLevel).isEqualTo(MemberLevel.JUNIOR);
    }
}

위와 같이 Mocking 할 객체에 @Mock 어노테이션을 명시하면 Mock 객체로 만들어 준다. 이제 memberRepository, levelCalculator 필드는 실제로 안이 비어있는 객체로 초기화 된다. 그런 다음 memberService를 @BeforeEach를 통해 Mock 객체와 연관을 맺으면 이제 필요한 연관 객체가 모두 초기화되었기 때문에 테스트가 가능하다.

다만 @Mock 어노테이션을 사용하기 위해서는 @ExtendWith(MockitoExtension.class)를 통해 MockitoExtension을 JUnit Extension에 추가해주어야 한다.

참고로 지금 처럼 Mock 객체를 테스트 메서드 전반에서 사용하지 않을 것이라면 필드로 선언할 필요는 없고, 아래와 같이 매개변수로 선언해 주어도 된다.

@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {

    @InjectMocks
    MemberService memberService;

    @BeforeEach
    void init(@Mock MemberRepository memberRepository,
              @Mock LevelCalculator levelCalculator) {
        memberService = new MemberService(memberRepository, levelCalculator);
    }

    @Test
    void memberServiceTest() {
        Long stubbedMemberId = 10L;
        MemberLevel memberLevel = memberService.updateLevel(stubbedMemberId);

        assertThat(memberLevel).isEqualTo(MemberLevel.JUNIOR);
    }
}

mock()

Mockito.mock() 메서드를 통해서도 Mock 객체를 만들 수도 있다.

	@BeforeEach
    public void init() {
        MemberRepository memberRepository = mock(MemberRepository.class);
        LevelCalculator levelCalculator = mock(LevelCalculator.class);
        memberService = new MemberService(memberRepository, levelCalculator);
    }

@InjectMocks

MemberService를 @BeforeEach를 통해 테스트 시작 전에 초기화 해줄 수도 있지만, 보다 간편한 방법으로는 @InjectMocks 어노테이션을 사용하는 방법이 있다.

@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {

    @InjectMocks
    MemberService memberService;

    @Mock
    MemberRepository memberRepository;

    @Mock
    LevelCalculator levelCalculator;

    @Test
    void memberServiceTest() {
        Long stubbedMemberId = 10L;
        MemberLevel memberLevel = memberService.updateLevel(stubbedMemberId);

        assertThat(memberLevel).isEqualTo(MemberLevel.JUNIOR);
    }
}

@InjectMocks 를 사용하게 되면 @Spy, @Mock 어노테이션이 붙은 객체를 자동으로 주입해준다.

이제 실제 테스트를 진행해보자. 당연히 실패가 발생할 것이다. 왜냐하면 Mock 객체로 만들기만 했지, 실제 테스트 데이터를 Stubbing 하지 않았기 때문이다.

    @Test
    void memberServiceTest() {
        Long stubbedMemberId = 10L;
        Optional<Member> byId = memberRepository.findById(stubbedMemberId);
        MemberLevel memberLevel = levelCalculator.calculateMemberLevel(10);
        
        System.out.println(byId); // Optional.empty
        System.out.println(memberLevel); // null
    }

데이터 자체를 Stubbing 하지 않아 실제 Mock 객체의 메서드들은 모두 null을 반환해버린다. MemberService의 updateLevel 메서드는 findById, calculateMemberLevel 메서드를 내부에서 호출한다. 그런데 호출 결과가 null이니 당연히 테스트가 실패한다. 따라서 시나리오에 맞는 데이터를 Stubbing 하는 작업을 진행한 다음에, 실제 updateLevel 메서드를 테스트해야 할 것이다.

Stubbing

모든 Mock 객체는 기본적으로 Stubbing을 하지 않으면 다음과 같이 작동한다.

  • 참조 타임을 리턴할 경우 Null을 리턴한다. (Optional 타입은 Optional.empty 리턴)
  • Primitive 타입을 리턴할 경우 기본 Primitive 값을 리턴한다.
  • Collection을 리턴할 경우 비어있는 콜렉션을 리턴한다.
  • Void 메서드는 아무런 일도 발생하지 않는다. (예외도 발생하지 않는다.)

따라서 메서드가 어떤 상황에서 어떤 행위를 할 것인지 지정하는 것이 Stubbing이다. 예를 들어 두 개의 인자를 받는 sum이라는 메서드가 있을 때 1과 2를 인자로 넘겨주면 3이라는 결과를 리턴하도록 조작하거나, 두 개의 인자를 받는 divide 메서드가 있을 때 두 번째 인자로 0을 넘겨주면 예외를 발생시켜준다던지 등등 Stubbing은 이러한 메서드의 행위를 정의하는 것이다.

그렇다면 이제 Stubbing을 통해서 MemberService의 updateLevel 메서드를 테스트해보자. Stubbing 방법을 모두 살펴볼 수는 없기 때문에 공식 문서를 참조하여 보도록 하자.

결과 리턴 (when - thenReturn)

    @Test
    void juniorTest() {
        // given
        Long memberId = 10L;

        // stub
        Integer stubbedMemberAge = 10;
        MemberLevel stubbedMemberLevel = MemberLevel.JUNIOR;
        Member stubbedMember = Member.builder()
                .id(10L)
                .age(stubbedMemberAge)
                .build();

        when(memberRepository.findById(eq(memberId)))
                .thenReturn(Optional.of(stubbedMember));

        when(levelCalculator.calculateMemberLevel(intThat(new AgeLt30Matcher())))
                .thenReturn(stubbedMemberLevel);

        // when
        MemberLevel memberLevel = memberService.updateLevel(memberId);

        // then
        assertThat(memberLevel).isEqualTo(stubbedMemberLevel);
    }

Mockito.when()을 이용하여 어떤 메서드가 호출이 되는지를 명시한다. 그리고 thenReturn()을 이용하여 어떤 값이 리턴되는지를 명시해주었다.

중간에 eq, intThat 메서드는 ArgumentMatchers에 정의된 정적 메서드이다. Stubbing 할 메서드의 인자값으로 어떤 값이 들어와야 하는지 조건을 명시할 수 있다. 여러 메서드가 정의되어 있으니 공식 문서를 참조하자.

첫 번째 Stubbing을 보면, MemberRepository의 findById로 10L과 일치하는 값이 들어올 경우, stubbedMember 객체를 반환하도록했다.

두 번째 Stubbing을 보면 AgeLt30Matcher 라는 커스텀 ArgumentMatcher를 통해 이 조건이 만족한다면 stubbedMemberLevel을 반환하도록 했다. 커스텀 ArgumentMatcher는 아래에 설명할 것이다.

따라서 MemberService의 updateLevel을 수행하면 우선 Stubbing된 Member 객체가 반환이 될 것이고, 이 객체의 age 필드 값은 10이기 때문에 30보다 작아 MemberLevel.JUNIOR를 반환하게 된다.

예외 던지기 (doThrow - when)

    @Test
    void dbExceptionTest() {
        // given
        Long memberId = 20L;

        // stub
        doThrow(new TransactionException("transaction ex"))
                .when(memberRepository).findById(eq(memberId));

        // when then
        assertThatThrownBy(() -> memberService.updateLevel(memberId))
                .isInstanceOf(TransactionException.class);
    }

doThrow 메서드를 통해 메서드가 특정 조건으로 호출될 경우 예외를 던지도록 Stubbing 할 수 있다.

여러번 Stubbing 하기

    @Test
    void repeatTest() {
        // given
        Long memberId = 10L;

        // stub
        Integer stubbedMemberAge = 50;
        MemberLevel stubbedMemberLevel1 = MemberLevel.JUNIOR;
        MemberLevel stubbedMemberLevel2 = MemberLevel.MASTER;
        Member stubbedMember = Member.builder()
                .id(10L)
                .age(stubbedMemberAge)
                .build();

        when(memberRepository.findById(eq(memberId)))
                .thenReturn(Optional.of(stubbedMember));

        when(levelCalculator.calculateMemberLevel(anyInt()))
                .thenReturn(stubbedMemberLevel1)
                .thenThrow(new RuntimeException())
                .thenReturn(stubbedMemberLevel2);

        // when then
        assertThat(memberService.updateLevel(memberId)).isEqualTo(stubbedMemberLevel1);
        assertThatThrownBy(() -> memberService.updateLevel(memberId)).isInstanceOf(RuntimeException.class);
        assertThat(memberService.updateLevel(memberId)).isEqualTo(stubbedMemberLevel2);
    }

when() 메서드에 조건을 명시하고, 결과를 다중으로 정의할 수 있다.

두 번째 Stubbing을 보면, thenReturn, thenThrow, thenReturn 순으로 결과를 정의했다. 그리고 assertThat에서 테스트를 검증할 때 3번을 수행하는데, 각각 정의된 순서대로 결과가 Stubbing된다.

ArgumentMatcher를 커스텀 하기

    @Test
    void customMatcherTest() {
        // given
        Long memberId = 10L;

        // stub
        Integer stubbedMemberAge = 50;
        MemberLevel stubbedMemberLevel = MemberLevel.MASTER;
        Member stubbedMember = Member.builder()
                .id(10L)
                .age(stubbedMemberAge)
                .build();

        when(memberRepository.findById(eq(memberId)))
                .thenReturn(Optional.of(stubbedMember));

        // 필드가 없는 경우
        when(levelCalculator.calculateMemberLevel(ArgumentMatchers.intThat(new AgeLt30Matcher())))
                .thenReturn(stubbedMemberLevel);
        
        // 필드가 있는 경우
        when(levelCalculator.calculateMemberLevel(intThat(new GoeMatcher(50))))
                .thenReturn(stubbedMemberLevel);

        // when
        MemberLevel memberLevel = memberService.updateLevel(memberId);

        // then
        assertThat(memberLevel).isEqualTo(stubbedMemberLevel);
    }

argThat, intThat, floatThat, longThat을 통해 커스텀한 ArgumentMatcher를 적용할 수 있다.

우선 필드가 없는 경우에 어떻게 커스텀을 하는지 보자.

public class AgeLt30Matcher implements ArgumentMatcher<Integer> {

    @Override
    public boolean matches(Integer argument) {
        if (argument < 30) {
            return true;
        }
        return false;
    }
}

메서드를 호출할 때 넘어오는 인자 값이 30 이하라면 true를 반환하고, 아니라면 false를 반환하는 AgeLt30Matcher이다.

ArgumentMatcher를 커스텀 하기 위해서는 ArgumentMatcher를 구현해야 하며, 여기서 타입변수는 메서드를 호출할 때 넘어오는 인자 값의 타입이다.

예를 들어, 위에서 calculateMemberLevel 메서드는 인자로 Integer 타입을 받는다. 따라서 Integer 타입의 타입변수를 명시해 준 것이다.

이러면 앞으로 들어오는 인자는 matches의 argument 매개변수로 들어오게 되고, 이를 비교해서 30 미만이라면 true를 반환해 조건을 만족한다고 알려주는 것이다.

만약에 여기서 30이라는 숫자 말고, 사용자가 커스텀한 ArgumentMatcher를 사용할 때마다 값을 지정해주고 싶다면 어떻게 해야할까? 바로 필드로 선언해주면 된다.

public class GoeMatcher implements ArgumentMatcher<Integer> {

    private Integer numToCompare;

    public GoeMatcher(Integer numToCompare) {
        this.numToCompare = numToCompare;
    }

    @Override
    public boolean matches(Integer argument) {
        if (argument >= numToCompare) {
            return true;
        }
        return false;
    }
}

이번에는 비교할 숫자를 필드로 선언하고, 생성자를 통해 초기화 하도록 설정했다. 그리고 비교할 숫자가 메서드의 인자값보다 작다면 true를 반환하도록 했다. 따라서 위에 테스트 코드를 보면, 두 번째 intThat을 사용하는 코드는 생성자를 통해 50이라는 숫자를 넘겨주고 있다. 이는 앞으로 인자가 50 이상인 경우 조건을 만족시키겠다는 소리이다.

번외 - argThat, intThat의 차이, argThat의 NullPointerException

처음에 공식 문서를 보았을 때, argThat을 통해 커스텀한 ArgumentMatcher를 만들 수 있다고 해서 무작정 따라해봤는데, 바로 NullPointerException이 발생했다. NullPointerException이 발생할 이유가 없었는데, 여기서 엄청 해맸다. 하지만 소스 코드를 보니 자세하게 설명이 나와있었다...

    /**
     * Allows creating custom argument matchers.
     *
     * <p>
     * This API has changed in 2.1.0, please read {@link ArgumentMatcher} for rationale and migration guide.
     * <b>NullPointerException</b> auto-unboxing caveat is described below.
     * </p>
     *
     * <p>
     * It is important to understand the use cases and available options for dealing with non-trivial arguments
     * <b>before</b> implementing custom argument matchers. This way, you can select the best possible approach
     * for given scenario and produce highest quality test (clean and maintainable).
     * Please read the documentation for {@link ArgumentMatcher} to learn about approaches and see the examples.
     * </p>
     *
     * <p>
     * <b>NullPointerException</b> auto-unboxing caveat.
     * In rare cases when matching primitive parameter types you <b>*must*</b> use relevant intThat(), floatThat(), etc. method.
     * This way you will avoid <code>NullPointerException</code> during auto-unboxing.
     * Due to how java works we don't really have a clean way of detecting this scenario and protecting the user from this problem.
     * Hopefully, the javadoc describes the problem and solution well.
     * If you have an idea how to fix the problem, let us know via the mailing list or the issue tracker.
     * </p>
     *
     * <p>
     * See examples in javadoc for {@link ArgumentMatcher} class
     * </p>
     *
     * @param matcher decides whether argument matches
     * @return <code>null</code>.
     */
    public static <T> T argThat(ArgumentMatcher<T> matcher) {
        reportMatcher(matcher);
        return null;
    }  

자바의 오토 박싱, 언박싱 과정에서 argThat을 사용할 경우 NullPointerException이 발생한다는 내용이다. 이러한 문제를 해결할 수 없어서 intThat, floatThat과 같은 오토 박싱, 언박싱이 일어나는 원시 타입을 위한 메서드를 정의해 놓았던 것이고, 이를 사용했더니 해결할 수 있었다.


Mock 객체 확인하기 - verify

Mockito에서는 Mock 객체의 메서드가 실행이 되었는지, 몇 번 실행되었는지, 어떤 순서로 실행되었는지 확인하는 기능도 제공해주고 있다.

    @Test
    void verifyTest() {
        // given
        Long memberId = 10L;

        // stub
        Integer stubbedMemberAge = 50;
        MemberLevel stubbedMemberLevel = MemberLevel.JUNIOR;
        Member stubbedMember = Member.builder()
                .id(10L)
                .age(stubbedMemberAge)
                .build();

        when(memberRepository.findById(eq(memberId)))
                .thenReturn(Optional.of(stubbedMember));

        when(levelCalculator.calculateMemberLevel(anyInt()))
                .thenReturn(stubbedMemberLevel);

        memberService.updateLevel(10L);

        // 기본 검증
        verify(levelCalculator, times(1)).calculateMemberLevel(anyInt()); // 해당 메서드가 1번 실행됐는지
        verify(levelCalculator, timeout(1000)).calculateMemberLevel(anyInt()); // 해당 메서드가 1000ms 안에 완료되었는지
        verify(levelCalculator, never()).calculateMemberLevel(anyInt()); // 해당 메서드가 전혀 실행되지 않는지
        verifyNoInteractions(levelCalculator); // 더 이상 어떤 행위도 수행하지 않는지

        // 순서도 검증
        InOrder inOrder = inOrder(levelCalculator);
        inOrder.verify(levelCalculator).calculateMemberLevel(anyInt());
        inOrder.verify(levelCalculator).calculateMemberLevel(anyInt());
    }

verify를 통해 Mock 객체, 혹은 특정 메서드가 몇 번 실행되었는지, 얼마 만에 완료되었는지, 실행되지 않았는지를 파악할 수 있다.

또한 InOrder를 통해서 호출 순서도 검증을 할 수가 있다. 지금은 MemberService에서 calculateMemberLevel 메서드를 하나만을 한번만 호출하지만, 다른 메서드도 호출한다고 가정하고 이를 순서대로 여러 번 호출하는 것을 확인해야 할 경우 활용할 수 있다.

이 외에도 더 많은 메서드를 제공하고 있는데 이는 공식 문서를 확인하자.

BDD Mockito

TDD 라는 말을 많이 들어봤을 것이다. 테스트를 먼저 작성하고, 테스트가 정상적으로 통과될 때까지 테스트를 하며 코드를 작성하는 방법론이다.

BDD는 Behaviour-Driven Development의 약자로, TDD의 한 종류이다. 말 그대로 행동 주도 개발인데 사용자의 행위, 즉 시나리오를 기반으로 테스트 케이스를 작성하는 방식으로 진행하는 TDD의 확장이다.

BDD는 기본적으로 Given -> When -> Then의 구조를 가지고 테스트를 설계하는 것을 권장한다.

  • Given: 테스트에서 구체화하고자 하는 행동을 시작하기 전에 테스트 상태를 설명하는 부분
  • When: 구체화하고자 하는 그 행동
  • Then: 어떤 특정한 행동 때문에 발생할거라고 예상되는 변화에 대한 설명

간단하게 어떤 값이 주어졌을 때(Given) 어떤 행위를 수행하면(When) 어떠한 결과가 나온다(Then)으로 구분하여 테스트를 작성할 수 있다. 하지만 그냥 Mockito를 사용하게 된다면, Given에 해당하는 문맥에 when() 메서드로 Stubbing을 해야 해서 혼동이 올 수 있다.

이를 보다 가시적으로 작성할 수 있게 되와주는 것이 BDD Mockito이다. 한번 코드를 보면서 두 코드가 어떤 차이점이 있는지 보자.

    @Test
    void bddTest() {
        // given
        Long memberId = 10L;

        // stub
        Integer stubbedMemberAge = 10;
        MemberLevel stubbedMemberLevel = MemberLevel.JUNIOR;
        Member stubbedMember = Member.builder()
                .id(10L)
                .age(stubbedMemberAge)
                .build();

//        when(memberRepository.findById(eq(memberId)))
//                .thenReturn(Optional.of(stubbedMember));
        given(memberRepository.findById(memberId))
                .willReturn(Optional.of(stubbedMember));

//        when(levelCalculator.calculateMemberLevel(intThat(new AgeLt30Matcher())))
//                .thenReturn(stubbedMemberLevel);
        given(levelCalculator.calculateMemberLevel(stubbedMemberAge))
                .willReturn(stubbedMemberLevel);

        // when
        MemberLevel memberLevel = memberService.updateLevel(memberId);

        // then
        assertThat(memberLevel).isEqualTo(stubbedMemberLevel);

//        verify(memberRepository, times(1)).findById(anyLong());
//        verifyNoMoreInteractions(memberRepository);
        then(memberRepository).should(times(1)).findById(anyLong());
        then(memberRepository).shouldHaveNoMoreInteractions();

//        verify(levelCalculator, times(1)).calculateMemberLevel(anyInt());
//        verifyNoMoreInteractions(levelCalculator);
        then(levelCalculator).should(times(1)).calculateMemberLevel(anyInt());
        then(levelCalculator).shouldHaveNoMoreInteractions();
    }  

그냥 Mockito를 사용했을 때는 문맥 상 given에 들어가야 하는 것이 맞지만, when() 메서드로 Stubbing을 하기 때문에 혼란이 올 수 있다. BDDMockito는 이를 given()으로 Stubbing을 하기 때문에 보다 테스트 코드를 이해하기 쉽다.

또한 then의 경우에도 Mockito는 verify() 등을 이용해야 하지만, BDDMockito는 then() 메서드를 통해 보다 어떤 결과가 나오는지 이해하기 쉽게 작성할 수 있다. 보다 더 자세한 문법은 공식 문서를 확인하자.

profile
반갑습니다. 주니어 백엔드 개발자 김영준입니다.

0개의 댓글