스프링을 사용하여 빈을 주입 받을때, 같은 타입(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해서 리스트에 담아 주입해야 하는데 어떻게 하면 되는걸까?
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());
}
그러면 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
을 적절하게 사용하는 것이 중요하다는 것을 알게 되었다. 이 포스트가 더 좋은 테스트 코드를 작성하는데 조금이나마 도움이 되길 바란다.