1

I have the following, heavily simplified class:

public class MyClass
{
    private MqttClient field;

    public void test() throws MqttException
    {
        field = new MqttClient(null, null);
        field.connect();
    }
}

I am using Eclipse Paho's MqttClient class.

I want to test the test method with a UnitTest. In order to have control over the MqttClient instance I want to mock the object. My test-class looks as follows:

public class TestMyClass
{
    // Make sure we are running with the PowerMockAgent initialized
    static
    {
        PowerMockAgent.initializeIfNeeded();
    }

    // Make sure we are using PowerMock
    @Rule
    public PowerMockRule    rule    = new PowerMockRule();

    // Mocked MqttClient
    @Mock
    MqttClient              field;

    // MyClass-Object injected with mocks.
    @InjectMocks
    MyClass                 myClass = new MyClass();

    @PrepareForTest(MyClass.class)
    @Test
    public void test() throws Exception
    {
        // Initialize our mocks from annotations
        MockitoAnnotations.initMocks(this);

        // Catch the "new MqttClient(null, null);"
        PowerMockito.whenNew(MqttClient.class).withAnyArguments().thenReturn(field);


        myClass.test();

    }
}

When I execute my test I am met with the following error:

java.lang.NullPointerException
    at test.java.test.MyClass.test(MyClass.java:13)
    at test.java.test.TestMyClass.test(TestMyClass.java:45)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.powermock.modules.junit4.rule.PowerMockStatement.evaluate(PowerMockRule.java:73)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)

I can call any method on field it is always a null-reference. My problem is I have no idea why this is the case.

I changed the type of field to several other classes, including java.awt.point and SerializationInstantiatorHelper (in order to test if it has to do with MqttClient being an external dependency) as well as several classes in my project, calling an appropriate method on those objects. In all those cases the problem did not exist and the test succeeded without any issues. The method on the mocked object is called and does nothing - just as I want it to.

Only when using the MqttClient class this problem occurs. Or at least I did not find any other class yet to reproduce this problem.

I am in all honesty totally lost at this point. What makes the MqttClient class special? Why can I seemingly not mock it like any other class? What did I miss?

//EDIT

I narrowed down the problem by creating a local version of MqttClient, copying over the source code. In all honesty, I am even more confused now though. The class has three constructors with the following signatures:

1.)

public MqttClient(  String serverURI,
                    String clientId) throws MqttException

2.)

public MqttClient(  String serverURI,
                    String clientId,
                    MqttClientPersistence persistence) throws MqttException

3.)

public MqttClient(  String serverURI,
                    String clientId,
                    MqttClientPersistence persistence,
                    ScheduledExecutorService executorService) throws MqttException

If one of those three constructors is removed, everything works. Any combination of the class with two of those three constructors is working just fine. As soon as all three constructors exist, the beforementioned error message appears. This leads me to the assumption it could have something to do with PowerMockito not finding the correct constructor? If so, how would that be avoidable?

Ben
  • 1,665
  • 1
  • 11
  • 22
  • Possible duplicate of [What is a NullPointerException, and how do I fix it?](https://stackoverflow.com/questions/218384/what-is-a-nullpointerexception-and-how-do-i-fix-it) – Anton Balaniuc May 22 '18 at 13:22
  • @AntonBalaniuc I am very well aware what a NullPointerException is and if you reread the question it would be arguable that this is a bad target for this duplication. Thank you for the input/idea though :) – Ben May 22 '18 at 13:29
  • Did you tried to specify exact constructor and parameters of MqttClient with PowerMock? It is not an answer, but I am just curious if it will help or not. – staszko032 May 22 '18 at 15:11

2 Answers2

1

The mocked target appears to have not been configured correctly so it is not behaving as expected when exercising the test.

Sometimes keeping things simple (KISS) is the way to go.

@RunWith(PowerMockRunner.class)
@PrepareForTest(MyClass.class)
public class TestMyClass {

    @Test
    public void test() throws Exception {
        //Arrange

        // mock MqttClient
        MqttClient field = mock(MqttClient.class);
        // setup the "new MqttClient(null, null);"
        PowerMockito.whenNew(MqttClient.class).withArguments(isNull(), isNull()).thenReturn(field);

        MyClass subject = new MyClass();

        //Act
        subject.test();

        //Assert
        PowerMockito.verifyNew(MqttClient.class).withArguments(isNull(), isNull());
        verify(field).connect();
    }
}

By using .withArguments with the appropriate arguments or matchers, it helps narrow down specifically which constructor is to be mocked for the test, avoiding any conflicts when multiple constructors exists.

Based on the simplified class under test shown in the example, there are no dependencies being injected as it initialized the dependency locally (i.e: tight coupling), so there really was no need to inject mocks into the class under test.

Also PrepareForTest is usually done on the test class and not the test method in this case, as according to documentation

All usages require @RunWith(PowerMockRunner.class) and @PrepareForTest annotated at class level.

emphasis mine

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thank you for the solution that fixed the problem! Could you maybe elaborate further **why** this fixes the problem and why the Annotation (which in theory should do the same thing) does not work here? Maybe draw from the other answer which had some input there. Also `withArguments(null, null)` is generally non advisable here as far as I know, I used `withArguments(Mockito.anyString(), Mockito.anyString())` which did the job without any warnings, etc. – Ben Aug 28 '18 at 08:20
  • @Ben I had not used `anyString` because of [what it said in the docs](https://static.javadoc.io/org.mockito/mockito-core/2.20.0/org/mockito/ArgumentMatchers.html#anyString--) about it matching non-null arguments and the fact that you were passing null for the arguments. So I suggested `null`, but the docs also suggested using the `isNull()` argument matcher. – Nkosi Aug 28 '18 at 10:35
  • Regarding your last statement that is not 100% correct, you can just as well use a PowerMockRule and the static initializer block I have in the original question over a PowerMockRunner. Why would you do that? Because else several code coverage tools are not able to measure the coverage anymore when the test runs (I do agree though that the PowerMockRunner is the way cleaner approach just sadly not always feasible depending on company policies on code coverage) – Ben Aug 28 '18 at 10:55
  • @Ben `Why would you do that? ` I've never had any issues with what I had learned from the docs, so I've never had any cause to change from that format so far. It kept things simple for me in such cases. – Nkosi Aug 28 '18 at 11:01
  • We are using JaCoCo for Maven to generate CodeCoverage reports for our Unit Tests (company policy). Unit tests run with the PowerMockRunner do not show up on the reports. So I am left without choice there sadly. – Ben Aug 28 '18 at 11:54
  • 1
    @Ben I would suggest reviewing the tight coupling in the current design. This would have all been avoid if the `MqttClient` was explicitly injected into the dependent class instead of manually creating the instance locally. The difficulty in testing it directly reflects a problem with the current design. If DI was used, you could have easily mocked the dependency and have it injected when testing. Would have made the code more SOLID. Hope I was of some help with your problem. – Nkosi Aug 28 '18 at 12:07
  • I totally agree and have mentioned several times before that we have a lot of hard to test code. Sadly nothing came of it so far but I will try to get the point across again I guess. – Ben Aug 28 '18 at 12:09
  • @Ben I understand. Been there before and know the feeling. Technical debt has a funny way of always calling in the loan even if you were not the one who took it out in the first place. :P Hard to test code is a sure code smell. – Nkosi Aug 28 '18 at 12:10
1

I think you're running to the same trouble as this reported in this issue

Mocking whenNew using withAnyArguments does not match null arguments #891

The problem is if multiple constructors are declared:

The first constructor's matchers are generated properly (using the form or(isA(Class), isNull())). But the remaining constructors are not (they only have any(Class) which causes them to not match against nullvalues).

So till the issue is addressed, you can use the work around:

MqttClient field = mock(MqttClient.class);

instead of @Mock MqttClient field;

Mạnh Quyết Nguyễn
  • 17,677
  • 1
  • 23
  • 51
  • Thank you for giving me an idea where the idea came from! Additionally to your change I had to change `withAnyArguments` to `withArguments(Mockito.anyString(), Mockito.anyString()` and got it working. – Ben Aug 28 '18 at 08:21