31

I've read plenty of articles about how to mock Spring's bean and their autowired fields. But there is nothing I could find about autowired lists of beans.

Concrete problem

I've a class called FormValidatorManager. This class loop through several validators which implements IFormValidator.

@Component
public class FormValidatorManager implements IValidatorManager {

    @Autowired
    private List<IFormValidator> validators;


    @Override
    public final IFieldError validate(ColumnDTO columnToValidate, String sentValue) {   
        String loweredColName = columnToValidate.getName().toLowerCase();
        IFieldError errorField = new FieldError(loweredColName);

        for (IEsmFormValidator validator : validators) {
            List<String> errrorsFound = validator.validate(columnToValidate, sentValue);

            //les erreurs ne doivent pas être cumulées.
            if(CollectionUtils.isNotEmpty(errrorsFound)){
                errorField.addErrors(errrorsFound);
                break;
            }
        }

        return errorField;
    }
}

I would like to test this class. But I can't find a way to mock validators property.

What I've tried

Since IFormValidators are singleton, I tried to mock several instances of these beans hoping them to be reflected in FormValidatorManager.validators but without success.

Then, I tried to create a list of IFormValidators which was annotated as @Mock. By initiating the List manually, I was hoping initMocks() to inject the created list. That was still without success.

Here is my last try:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:spring/test-validator-context.xml"})
public class FormValidatorManagerTest {

    @Mock
    private RegexValidator regexValidator;

    @Mock
    private FormNotNullValidator notNullValidator;

    @Mock
    private FormDataTypeValidator dataValidator;

    @InjectMocks
    private FormValidatorManager validatorManager;

    @Mock
    private List<IEsmFormValidator> validators = new ArrayList<IEsmFormValidator>();

    @Mock
    private ColumnDTO columnDTO;

    @Before
    public void init() {

        validators.add(notNullValidator);
        validators.add(regexValidator);
        validators.add(dataValidator);

        MockitoAnnotations.initMocks(this);

        Mockito.when(columnDTO.getTitle()).thenReturn("Mock title");
        Mockito.when(columnDTO.getName()).thenReturn("Mock name");
    }



    @Test
    public void testNoErrorFound(){
        mockValidator(notNullValidator,  new ArrayList<String>());
        mockValidator(regexValidator,  new ArrayList<String>());
        mockValidator(dataValidator,  new ArrayList<String>());

        IFieldError fieldErrors = validatorManager.validate(columnDTO, "Not null value");

        Assert.assertEquals(0, fieldErrors.getErrors().size());

        verifyNumberOfValidateCalls(regexValidator, Mockito.atMost(1));
        verifyNumberOfValidateCalls(dataValidator, Mockito.atMost(1));
        verifyNumberOfValidateCalls(notNullValidator, Mockito.atMost(1));
    }



    private void mockValidator(IFormValidator validator, List<String> listToReturn){
        Mockito.when(validator.validate(Mockito.any(ColumnDTO.class), Mockito.anyString())).thenReturn( listToReturn );
    }

    private void verifyNumberOfValidateCalls(IFormValidator validator, VerificationMode verifMode){
        Mockito.verify(validator, verifMode).validate(Mockito.any(ColumnDTO.class), Mockito.anyString());
    }
}

An NPE is thrown in IFormValidator.validate() which I thougth would be mocked. The concrete implementation should not be called.

This leads to a really bad behavior since some of my tests on that class are false positives while others completly fail.

I'm trying to figure out how to mock an autowired list of beans while still having the possibility to mock specific implementations.

Do you have an idea start of solution ?

Regards

Grégory Elhaimer
  • 2,731
  • 1
  • 16
  • 21
  • How would you expect `@Mock` and `@Autowired` on single field to work? Do you want it mocked, or auto wired? Also creating a list in your test and hoping spring to detect it is never going to happen. Create mocks in your spring configuration instead of your classes, use the `mock` factory method of `Mockito` for that. – M. Deinum Oct 23 '15 at 12:29
  • I actually want it mocked, but since it's a spring bean, shouldn't in be autowired as well ? I actually tried to remove the autowired, but it did not work as well. It's even worse since the validators are null and not mocked. – Grégory Elhaimer Oct 23 '15 at 12:30
  • you can use @Autowired above constructor. So in test you could pass validators as constructor parameters to tested class. – hi_my_name_is Oct 23 '15 at 12:37
  • I must not change the tested class implementation – Grégory Elhaimer Oct 23 '15 at 12:38
  • It is either a mock or a spring bean it cannot be both. You can have your mock auto wired but that will only work if you define your Mocks as spring beans. And you might want to read http://olivergierke.de/2013/11/why-field-injection-is-evil/ and reconsider field injection. I would make the field final and simply create a constructor. That way you can really make it a unit test and construct the object yourself and pass anything you want into the constructor and you increase the code quality imho. – M. Deinum Oct 23 '15 at 12:44

3 Answers3

46

I finally figured it out...

Sometimes, asking a question can give you a better approach to your problems :p

The problem is I was linking the validators to the list before they were mocked. The validators was then null and no reference could be updated when the MockitAnnotations.initMocks(this) was called.

Moreover, to avoid iterator problems on List, I had to use @Spy instead of @Mock.

Here is the final solution:

@Mock
private EsmRegexValidator regexValidator;

@Mock
private EsmFormNotNullValidator notNullValidator;

@Mock
private EsmFormDataTypeValidator dataValidator;

@InjectMocks
private EsmFormValidatorManager validatorManager;

@Spy
private List<IEsmFormValidator> validators = new ArrayList<IEsmFormValidator>();

@Mock
private ColumnDTO columnDTO;

@Before
public void init() {

    MockitoAnnotations.initMocks(this);

    validators.add(notNullValidator);
    validators.add(regexValidator);
    validators.add(dataValidator);

    Mockito.when(columnDTO.getTitle()).thenReturn("Mock title");
    Mockito.when(columnDTO.getName()).thenReturn("Mock name");
}
Grégory Elhaimer
  • 2,731
  • 1
  • 16
  • 21
  • 1
    This solution seems to have its limits. When dealing with 2 List of Beans. It does not inject beans correctly. repositories.add(emailRepository); parsers.add(smsParser); Now in the actual class code - when using repositories - it consists of parser mocks -.- Damn that's a pretty old answer tho. Must see if anything new has come up. – kristjan reinhold Nov 02 '21 at 11:39
0

Adding another answer when dealing with multiple list of beans. Mockito doesnt know anything about the generics it just uses random list provided so in my case something like this happened.

enter image description here

Which threw the ClassCastException because the bean injection was not perfomed correctly. Expecting SfmcImportRepository but injection was SfmcParser

  @Mock SfmcEmailsCsvFileParser emailParser;
  @Mock SfmcSmsCsvFileParser smsParser;
  @Mock SfmcSmsRepository smsRepository;
  @Mock SfmcEmailRepository emailRepository;

  List<SfmcImportRepository> sfmcImportRepositories = new ArrayList<>();
  List<SfmcParser> sfmcParsers = new ArrayList<>();
  SfmcFtpService service;

  @Before
  public void init() {
    sfmcImportRepositories.add(emailRepository);
    sfmcImportRepositories.add(smsRepository);
    sfmcParsers.add(smsParser);
    sfmcParsers.add(emailParser);
    service = new SfmcFtpService(sfmcImportRepositories, sfmcParsers);
  }
kristjan reinhold
  • 2,038
  • 1
  • 17
  • 34
0

method initMocks is deprecated in the recent versions and is no longer needed:

@Mock
private SomeTxHandler1 txHandler1;
@Mock
private SomeTxHandler2 txHandler2;

@Spy
private final List<TxHandler> txHandlers = new ArrayList<>();

@Spy // if you want to mock your service's methods
@InjectMocks
private MyService myService;

@BeforeEach
public void init() {
    lenient().when(txHandler1.accept(...)).thenReturn(true);
    txHandlers.add(txHandler1);
    lenient().when(txHandler2.accept(...)).thenReturn(true);
    txHandlers.add(txHandler2);
}
Denis Orlov
  • 130
  • 4