본문 바로가기

💻 개발/Java

Mockito : Mock 리스트를 주입하고 테스트 하기

상황

스프링을 사용하여 빈을 주입 받을때, 같은 타입(interface)을 구현한 빈들을 아래와 같이 컬렉션으로 주입 받아 사용하는 경우가 있다. 

public interface Validator {
    void validate(Order order);
}
@Service
public class OrderValidationService {
    @Autowired
    private List<Validator> validatorList;

    public void validate(Order order) {
        for (Validator component : validatorList) {
            component.validate(order);
        }
    }
}

단위 테스트 프레임워크로 JUnit을 사용하고 Mockito 라이브러리를 사용할 때, OrderValidationService의 단위 테스트는 어떻게 작성할 수 있을까? 

단순하게 생각하면 아래의 코드 처럼 의존하고 있는 validatorList를 Mock 객체로 대체하여 주입한 뒤 테스트 하는 방법을 떠올릴 수 있다. (사실 내가 그랬다.)

@RunWith(MockitoJUnitRunner.class)
public class OrderValidationServiceTest {
    @InjectMocks
    private OrderValidationService sut;
    @Mock
    private List<Validator> validatorList;

    @Test
    public void testValidate() {
        sut.validate(new Order());
    }
}

그러나 이 방법은 어딘가 이상하다. 일단 테스트가 실패하기도 하고, List<Validator>타입에 대한 객체를 mocking 한 것이기 때문에 Validator에 대항 행위를 mocking 해야되는 것이 아니라 List에 대한 행위를 mocking 해야 한다. 

 

이쯤에서 OrderValidationService가 의존 하는 대상이 무엇인지 한 번 생각 해 볼 필요가 있는 것 같다. 

OrderValidationService는 List<Validator>타입의 객체에 의존 한다고도 볼 수 있지만, 다르게 생각하면 Validator 타입의 객체 여러개(?)에 의존 한다고 볼 수도 있다.

사실 빈을 주입 받을 때도 List<Validator> 타입의 빈을 주입 받은 것이 아니라, 여러개의 Validator 타입의 빈들을 List에 담아서 주입 받은 것이기 때문에, 후자의 관점으로 보는게 더 맞다고 생각한다. 

 

그렇다면 테스트 코드를 작성할 때도 Validator타입의 객체를 mocking해서 리스트에 담아 주입해야 하는데 어떻게 하면 되는걸까?

 

@Spy

Mockito 라이브러리의 @Spy 어노테이션을 사용하면 우리가 원하는 mock 주입을 해줄 수 있다.

@RunWith(MockitoJUnitRunner.class)
public class OrderValidationServiceTest {
    @InjectMocks
    private OrderValidationService sut;
    @Spy
    private List<Validator> validatorList = new ArrayList<>();
    @Mock
    private Validator mockValidatorA;
    @Mock
    private Validator mockValidatorB;

    @Before
    public void setUp() {
        validatorList.add(mockValidatorA);
        validatorList.add(mockValidatorB);
    }

    @Test
    public void testValidate() {
        sut.validate(new Order());
    }
}

먼저 List<Validator> 타입의 필드에 @Mock이 아닌 @Spy 어노테이션을 붙혀주고 ArrayList로 초기화를 해준다.

@Spy
private List<Validator> validatorList = new ArrayList<>();

그런 다음 mocking 하고자 하는 타입(여기서는 Validator)에 대해 @Mock 어노테이션으로 mock 객체를 생성하고 setUp 메서드(@Before)에서 해당 mock 객체들을 validatorList에 추가한다.

@Before
public void setUp() {
    validatorList.add(mockValidatorA);
    validatorList.add(mockValidatorB);
}

이제 우리가 의도한 대로 List의 행위가 아닌 Validator의 행위를 mocking할 수 있다.

예를 들어 아래 코드처럼 mockValidatorA가 예외를 던지는 상황을 재현해서 테스트를 돌려볼 수 있다.

@Test(expected = RuntimeException.class)
public void testValidate() {
    // given
    willThrow(new RuntimeException()).given(mockValidatorA).validate(any());
    
    // when
    sut.validate(new Order());
}

 

@Spy와 @Mock의 차이

그러면 Mockito 라이브러리에서 제공하는 @Spy와 @Mock은 어떤 차이가 있는걸까?

 

둘의 가장 큰 차이점은 @Spy는 실제 인스턴스를 사용해서 mocking을 하고, @Mock은 실제 인스턴스 없이 가상의 mock 인스턴스를 직접 만들어 사용한다는 것이다. 그래서 @Spy는 Mockito.when() 이나 BDDMockito.given() 메서드 등으로 메서드의 행위를 지정해 주지 않으면 @Spy 객체를 만들 때 사용한 실제 인스턴스의 메서드를 호출한다.

@Spy
private List<Validator> validatorList = new ArrayList<>();

@Spy 객체를 선언한 코드에서 볼 수 있듯이, @Spy 객체는 반드시 실제 인스턴스가 필요하기 때문에 ArrayList로 초기 값을 설정 해주었던 것이다. 만약 인스턴스를 초기화 해주지 않으면 아래와 같은 에러가 발생한다.

org.mockito.exceptions.base.MockitoException: Unable to initialize @Spy annotated field 'validatorList'.
Type 'List' is an interface and it cannot be spied on.

 

마치며

사실 Mockito 라이브러리를 사용할 때 맹목적으로 @Mock 어노테이션만을 사용 했었는데, 이번 사례를 통해 상황에 따라 @Spy와 @Mock을 적절하게 사용하는 것이 중요하다는 것을 알게 되었다. 이 포스트가 더 좋은 테스트 코드를 작성하는데 조금이나마 도움이 되길 바란다.