We have a method to test which iterates over some List. The example is a validator service which iterates over a List of validators applying each one and returning when one fails ie. it returns an Optional
containing an error message (this is just a concrete example to demonstrate Mockito and is not important to understand).
public class SomeValidationService implements ValidationService {
/* List of validators which all encapsulate some different business validation logic */
/* This gets injected say via Spring */
private List<Validator> validators;
public Optional<ErrorMessage> validateAction(final ObjectToValidate obj) {
for (Validator validator: validators) {
Optional failure = validator.validate(obj);
if (failure.isPresent()) {
return failure;
}
}
return Optional.empty();
}
}
And of course we have a Validator
interface and some implementations.
public interface Validator {
Optional<ErrorMessage> validate(ObjectToValidate object);
}
public class ValidatorOne implements Validator {
@Override
public Optional<ErrorMessage> validate(final ObjectToValidate object) { ... }
}
public class ValidatoTwo implements Validator {
@Override
public Optional<ErrorMessage> validate(final ObjectToValidate object) { ... }
}
I originally tried to Mock just the List<Validator>
but this fails in the for
loop. It turns out that we have to mock and inject the Iterator that the loop must use as well. This is achieved as below.
@RunWith(MockitoJUnitRunner.class)
public class SomeValidationServiceTest {
@Mock
private ValidatorOne validatorOne;
@Mock
private ValidatorTwo validatorTwo;
@Mock
private final List<Validator> validators = new ArrayList<Validator>() { {
add(validatorOne);
add(validatorTwo);
}};
@Mock
private Iterator<Validator> validatorIterator;
@Mock
ObjectToValidate objectToValidate
// Inject the above Mocks into the class under test
@InjectMocks
private SomeValidationService validationService;
private static Optional<ErrorMessage> successfulValidation = Optional.empty();
private static Optional<ErrorMessage> validationOneFailure = Optional.of(new ErrorMessage(...));
private static Optional<ErrorMessage> validationTwoFailiure = Optional.of(new ErrorMessage(...));
@Before
public void setUp() {
// We have to mock what happens when the for loop internally looks up the iterator
Mockito.when(validators.iterator()).thenReturn(validatorIterator);
// We have to mock what we want to happen when the for loop calls the next and hasNext
// operations of the Iterator
Mockito.when(validatorIterator.hasNext()).thenReturn(true, true false);
Mockito.when(validatorIterator.next())
.thenReturn(validatorOne)
.thenReturn(validatorTwo);
// Setup the success cases as the defaults
given(validatorOne.validate(objectToValidate)).willReturn(successfulValidation);
given(validatorTwo.validate(objectToValidate)).willReturn(successfulValidation);
}
@Test
public void testValidationSuccess() {
Optional<ErrorMessage> validation = validationService.validateAction(objectToValidate);
assertThat(validation).isEqualTo(Optional.empty());
}
@Test
public void testValidationOneFailure() {
// override the success case with a failure case for validator One
given(validatorOne.validate(objectToValidate)).willReturn(validationOneFailure);
Optional<ErrorMessage> validation = validationService.validateAction(objectToValidate);
assertThat(failure.isPresent()).isTrue();
assertThat(failure.get()).isInstanceOf(ErrorMessage.class);
assertThat(failure.get().getMessageId()).isEqualTo(VALIDATION_ONE_FAILURE_MESSAGE_ID);
}
@Test
public void testValidationTwoFailure() {
// override the success case with a failure for validator Two
given(validatorTwo.validate(objectToValidate)).willReturn(validationTwoFailure);
Optional<ErrorMessage> validation = validationService.validateAction(objectToValidate);
assertThat(failure.isPresent()).isTrue();
assertThat(failure.get()).isInstanceOf(ErrorMessage.class);
assertThat(failure.get().getMessageId()).isEqualTo(VALIDATION_TWO_FAILURE_MESSAGE_ID);
}
}