Spring/테스트 코드

[Test] ArgumentCaptor 란?

hanrabong 2023. 12. 18. 18:03

배경

 테스트 코드를 짜면서 Stubbing을 통해서 제가 원하는 대로 코드를 조작할 수 있었습니다. 또한 해당 메서드의 Stubbing이 잘 동작하는지 Mockito.verify, BDDMockito.then 을 이용하여 해당 메서드가 몇 번 호출이 되었고 어떤 인자를 갖고 호출이 되었는지 확인할 수 있었습니다.

 점차 다양한 테스트 코드를 짜면서 해당 코드의 인자의 값이 로직에서 의도한 대로 세팅이 되었는지 확인을 필수로 해야하는 경우가 생겼습니다. 이 때 자주 사용했던 ArgumentCaptor 라는 클래스에 대해 알아보도록 하겠습니다.

 

 

이 글에서 사용한 코드는 해당 github에 저장되어 있습니다.

테스트 코드

 

GitHub - Rabongg/Test-Junit: Junit을 이용한 test 예제

Junit을 이용한 test 예제. Contribute to Rabongg/Test-Junit development by creating an account on GitHub.

github.com

 

 

 

ArgumentCaptor

ArgumentCaptor를 보면 Argument + Captor 라고 생각이 들 것입니다. Argument는 인수라고 하며 우리가 함수를 실행할 때 넘기는 값들을 말합니다.

 

그럼 Captor의 뜻은 무엇일까요. Captor의 사전적 의미를 보면 다음과 같습니다. 

Captor
명사) 포획자

 

이 뜻으로 유추를 해보면 인수를 포획한다 라고 대충 이해가 될 것입니다. 실제로 ArgumentCaptor를 이용하여 내가 원하는 대로 값이 넘어가는지 확인할 수 있습니다.

 

 

사용법

 선언하고 사용하는 법은 다음과 같습니다. @Captor 를 붙여서 인스턴스 변수처럼 사용할 수도 있고 로컬 변수처럼 하나의 메서드 안에서만 사용할 수도 있습니다.

public Class Test {
    @Captor
    ArgumentCaptor<User> userArgumentCaptor;   // 인스턴스 변수
    
    
    public void test() {
        // 로컬 변수
        ArgumentCaptor<User> userArgumentCaptor1 = ArgumentCaptor.forClass(User.class);
    }
}

 

※ 참고 ※
Captor, ArgumentCaptor 전부 mockito 라이브러리에 포함이 되어있기에 mockito 라이브러리를 의존성으로 추가를 했으면 따로 설정을 해줄 필요가 없습니다.

 

 

ArgumentCator 클래스 내부는 다음과 같습니다.

@CheckReturnValue
public class ArgumentCaptor<T> {

    private final CapturingMatcher<T> capturingMatcher = new CapturingMatcher<T>();
    private final Class<? extends T> clazz;

    private ArgumentCaptor(Class<? extends T> clazz) {
        this.clazz = clazz;
    }

    public T capture() {
        T ignored = Mockito.argThat(capturingMatcher);
        return defaultValue(clazz);
    }

    public T getValue() {
        return this.capturingMatcher.getLastValue();
    }

    public List<T> getAllValues() {
        return this.capturingMatcher.getAllValues();
    }

    public static <U, S extends U> ArgumentCaptor<U> forClass(Class<S> clazz) {
        return new ArgumentCaptor<U>(clazz);
    }
}

 

IDE에서 ArgumentCaptor 내부를 보면 각 메서드마다 설명도 잘 적혀 있고 예시 코드도 잘 적혀 있으니 꼭 참고 하시기 바랍니다. 간단히 메서드에 대해 설명하겠습니다.

 

 ArgumentCaptor 내부 메서드

capture(): 말 그대로 해당 인자를 낚아채는 메서드입니다.
getValue(): 낚아챈 인자의 값을 불러올 때 사용합니다. 만약 해당 메서드가 여러번 호출이 되었을 경우 가장 마지막으로 호출된 메서드의 인자를 갖고옵니다.
getAllValues(): 낚아챈 인자 전부를 List 형태로 받아옵니다.

 

 

이제 사용하는 방법을 알았으니 코드를 통해 어떻게 보통 사용하는지 확인해보겠습니다.

 

 

코드 예시

// User.java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@AllArgsConstructor
public class User {

    private String name;

    private String state;
}

// UserService.java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserDao userDao;

    public void deleteUser(User user) {
        user.setState("DELETED");
        userDao.saveUser(user);
    }

    public void createUser(User user) {
        user.setState("CREATED");
        userDao.saveUser(user);
    }
}

 

다음과 같은 코드가 있다고 가정해보겠습니다. 코드에 대해서 짤막하게 설명을 하자면, User라는 객체를 저장하고 삭제하는 로직입니다. 실제 저장, 삭제를 하는 것이 아닌 저장을 할 때는 상태를 "CREATED"로 삭제를 할 때는 상태를 "DELETED" 상태로 저장을 하게됩니다.

 

 

deleteUser, createUser 에 대한 테스트 코드를 작성해 보겠습니다.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserDao userDao;

    @InjectMocks
    private UserService subject;

    @Captor
    ArgumentCaptor<User> userArgumentCaptor;
    
    @Test
    void testDeleteUser() {
        User user = new User("rabong", "CREATED");

        subject.deleteUser(user);

        verify(userDao, times(1)).saveUser(userArgumentCaptor.capture());

        User value = userArgumentCaptor.getValue();
        assertEquals("DELETED", value.getState());
    }

    @Test
    void testCreateUser() {
        User user = new User("rabong", "CREATING");

        subject.createUser(user);

        ArgumentCaptor<User> userArgumentCaptor1 = ArgumentCaptor.forClass(User.class);
        verify(userDao, times(1)).saveUser(userArgumentCaptor1.capture());
        User value = userArgumentCaptor1.getValue();
        assertEquals("CREATED", value.getState());
    }
}

 

 다음과 같이 테스트 코드를 작성할 수 있습니다. 먼저 테스트 코드를 짜기 전에 항상 '어떤 기능에 대한 테스트를 해야할까' 에 대해 생각을 해야합니다. deleteUser, createUser를 보면 state 값만 다르다는 것을 알 수 있습니다. 따라서 해당 테스트에서는 setState()에 값이 원하는 대로 들어가는지를 필수적으로 확인을 해야합니다.

 

saveUser 메서드가 실행될 때 User를 인자로 넘기기에 이 때 User 인자를 ArgumentCapture를 이용해서 낚아챘습니다. 그리고 해당 인자의 state 값이 무엇인지 assertEquals 메서드를 이용해서 비교를 했습니다.

 

 

마치며...

 이번 글에서 ArgumentCaptor를 이용해서 mock 객체 메서드에 어떤 인자가 넘어가는지 확인해보았습니다. 항상 테스트 코드를 작성할 때, 단지 라인 커버리지를 채우자는 생각보다는 해당 메서드에서 어떤 기능을 확인해야하고 어떤 것을 검증해야하는지 생각하며 작성하고 있습니다.
 Mockito.verify, BDDMockito.then 을 이용하여 몇 번 호출이 되었는지 어떤 인자가 넘어가는지 검증을 할 수 있지만, ArgumentCaptor 를 이용하면 인자를 캡쳐해서 정확히 의도대로 인자가 넘어가는지 자세하게 확인을 할 수 있습니다. 

다음 글에서도, 테스트 코드를 짤 때 사용하면 좋은 기능들에 대해서 적어보겠습니다.

'Spring > 테스트 코드' 카테고리의 다른 글

[Test] Mock, Spy  (0) 2023.12.04
[Test] 테스트 코드를 왜 작성해야할까?  (0) 2023.10.28