1

I have a spring boot app with autowired components. It works fine operationally, but mocking is not initializing the components properly. Here are the main components:

base processor:

@Component
public class Processor {
    public String getType() { return null; }

    public void transformInput(JsonObject data) { }

    public void buildOutputRequest(String syncType) { }
}

, one of the 4 subtypes:

@Component
public class ProcessorType1 extends Processor {

    @Override
    public String getType() { return Type.TYPE1.getValue();}

    public void transformInput(JsonObject data) {
        // do dtuff
    }
}

, supporting enum:

public enum Type {
    TYPE1("TYPE1"),TYPE2("TYPE2"), TYPE3("TYPE3"), TYPE4("TYPE4");

    private final String value;
    public static final Map<Type, String> enumMap = new EnumMap<Type, String>(
        Type.class);

    static {
        for (Type e : Type.values())
            enumMap.put(e, e.getValue());
    }

    Type(String value) { this.value = value;}

    public String getValue() { return value; }
}

,and the factory class:

@Component
public class ProcessorFactory {

    @Autowired
    public ProcessorFactory(List<Processor> processors) {
        for (Processor processor : processors) {
            processorCache.put(processor.getType(), processor);
        }
    }

    private static final Map<String, Processor> processorCache = new HashMap<String, Processor>();

        public static Processor getProcessor(String type) {
            Processor service = processorCache.get(type);
            if(service == null) throw new RuntimeException("Unknown  type: " + type);
                return service;
        }
    }

, then operationally, a calling service uses the factory similar to this:

 @Service
 MyService {

    public processData (
        // do stuff to get processor type and data
        Processor processor = ProcessorFactory.getProcessor(type);
                processor.transformInput(data);
    )
}

Again, operationally this works fine. However, I attempt to mock the factory and its initialization like the following:

@RunWith(SpringJUnit4ClassRunner.class)
public class ProcessorFacTest
{

    @InjectMocks
    ProcessorFactory factory;

    @Test
    void testGetSyncProcessorDocument() {
        String type = Type.TYPE1.getValue();
        Processor processor = ProcessorFactory.getProcessor(type);
        Assert.assertTrue(processor instanceof ProcessorType1);
    }
}

My expectation was that since I am using @InjectMocks, and since ProcessorFactory has its constructor autowired, the constructor would be called by InjectMocks as part of the initialization. However, this is not happening. The processorCache is zero-length because the constructor is never called.

I realize I am mixing injection with static usages, but since it worked operationally, and since my understanding was that InjectMocks would handle the creation of the processorCache, that it should have worked for the test class as well, and its not.

I would be grateful for any ideas as to what I am doing wrong. Thank you

Timothy Clotworthy
  • 1,960
  • 2
  • 19
  • 42

1 Answers1

0

You never define any mocks that could be injected. Which dependencies should be autowired if you don't define any? The constructor is still called, but with an empty list of dependencies (use your debugger to set a breakpoint there and check for yourself).

It's also critical to note that mocks will return null, empty lists or 0/false by default for all methods, unless the mock has been set up properly.

You must define the mocks so that they can be picked up by @InjectMocks plus use the correct JUnit runner/extension:

@ExtendWith(MockitoExtension.class)   // JUnit 5
// @RunWith(MockitoJUnitRunner.class) // JUnit 4
public class ProcessorFacTest
{
    @Mock
    ProcessorType1 processor1;
    @Mock
    ProcessorType2 processor2;
    // ...

    @InjectMocks
    ProcessorFactory factory;

    @BeforeEach // JUnit 5
    // @Before  // JUnit 4
    void setup() {
      Mockito.when(processor1.getType()).thenReturn(Type.TYPE1);
      Mockito.when(processor2.getType()).thenReturn(Type.TYPE2);
    }

    @Test
    void testGetSyncProcessorDocument() {
        String type = Type.TYPE1.getValue();
        Processor processor = ProcessorFactory.getProcessor(type);
        Assert.assertTrue(processor instanceof ProcessorType1);
    }
}

EDIT: the above won't work because the processors are not a direct dependency for your class under test, but are elements of a list. Mockito will not be able to inject the correct mocks.

And here's how you would write the test as a Spring test with mock beans:

@RunWith(SpringJUnit4ClassRunner.class)
public class ProcessorFacTest
{
    @MockBean
    ProcessorType1 processor1;
    @MockBean
    ProcessorType2 processor2;
    // ...

    @Autowired
    ProcessorFactory factory;

    @BeforeEach // JUnit 5
    // @Before  // JUnit 4
    void setup() {
      Mockito.when(processor1.getType()).thenReturn(Type.TYPE1.getValue());
      Mockito.when(processor2.getType()).thenReturn(Type.TYPE2.getValue());
    }

    @Test
    void testGetSyncProcessorDocument() {
        String type = Type.TYPE1.getValue();
        Processor processor = ProcessorFactory.getProcessor(type);
        Assert.assertTrue(processor instanceof ProcessorType1);
    }
}

But actually you don't need to use any annotations here, you could wire the dependencies manually just as simple:

public class ProcessorFacTest
{
    ProcessorFactory factory;

    @BeforeEach // JUnit 5
    // @Before  // JUnit 4
    void setup() {
      final ProcessorType1 processor1 = Mockito.mock(ProcessorType1.class);
      final ProcessorType1 processor2 = Mockito.mock(ProcessorType2.class);
      Mockito.when(processor1.getType()).thenReturn(Type.TYPE1);
      Mockito.when(processor2.getType()).thenReturn(Type.TYPE2);

      factory = new ProcessorFactory(
          List.of(
              processor1,
              processor2,
              // ...
          ));
    }    

    @Test
    void testGetSyncProcessorDocument() {
        String type = Type.TYPE1.getValue();
        Processor processor = ProcessorFactory.getProcessor(type);
        Assert.assertTrue(processor instanceof ProcessorType1);
    }
}

If your processors are simply, easy to instantiate and don't depend on other classes, it might even be simpler (and better, YMMV) to simply use the real collaborators instead of mocks.

And it might be easier to create a test implementation of your processor type, which can be easier to set up than a mock object created dynamically with Mockito. The code would look like this:

    @BeforeEach // JUnit 5
    // @Before  // JUnit 4
    void setup() {
      factory = new ProcessorFactory(
          List.of(
              new TestProcessor(Type.TYPE1),
              new TestProcessor(Type.TYPE2),
              // ...
          ));
    }

    private static class TestProcessor extends Processor {
      private final Type type;
      public TestProcessor(final Type type) { this.type = type; }
      public String getType() { return type.getValue(); }

      // Base class methods do nothing already. If they were, you would override them here with an empty implementation:
      @Override
      public void transformInput(JsonObject data) { /* do nothing */ }
      @Override
      public void buildOutputRequest(String syncType) { /* do nothing */ }
    }

NB. It's bad practice to use names that are already defined in the JDK. Type is one such name (java.lang.reflect.Type).

It might also be worthwhile to read Why is my class not calling my mocked methods in unit test? to get a good overview of common gotchas when setting up mocks.

knittl
  • 246,190
  • 53
  • 318
  • 364
  • ok, I added the Mocks as per your suggestion above, however it is having no affect at all. The constructor of the factory class still is not being called. – Timothy Clotworthy Dec 26 '22 at 15:09
  • @TimothyClotworthy I took a second look: you were using the wrong runner/extension. This is not a spring test, so why use the Spring runner? If you require Mockito, you must use the Mockito runner/extension. – knittl Dec 26 '22 at 15:12
  • ok I added both the ExtendWIth and the RunWith you have above. So the constructor is indeed being called now. However, the "processors" (List passed into constructor) is null. I have all 5 types of processors mocked (the base and the 4 subclasses). Thanks – Timothy Clotworthy Dec 26 '22 at 15:23
  • @TimothyClotworthy sorry, you should only use one of the annotations (depending on your JUnit version), not both – knittl Dec 26 '22 at 15:25
  • @kinttl, ok, for me MockitoExtension is better, however the "processors" is still null when the constructor is called. I must be still missing something. Thanks – Timothy Clotworthy Dec 26 '22 at 15:39
  • @kinttl Ah ok, sorry. I did not notice update to your answer. That is much better. Still now its not mocking the enum. – Timothy Clotworthy Dec 26 '22 at 16:19
  • Sorry, meant to say it is better, but it is not adding the subtypes to the processorCache. Thanks. – Timothy Clotworthy Dec 26 '22 at 16:34
  • @TimothyClotworthy can you debug? Is the constructor being called? Is the list non-empty? Are the mocks initialized? I have a hunch: Mockito cannot detect wiring via lists (it's not Spring dependency injection). Use the second approach from my answer: manually wiring your class under test (without any annotations). – knittl Dec 26 '22 at 16:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250659/discussion-between-timothy-clotworthy-and-knittl). – Timothy Clotworthy Dec 26 '22 at 16:59