尝试在模拟的 Eclipse Paho MqttClient 上调用方法时出现 NullPointerException

NullPointerException when trying to call method on mocked Eclipse Paho MqttClient

我有以下经过高度简化的 class:

public class MyClass
{
    private MqttClient field;

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

我正在使用 Eclipse Paho's MqttClient class。

我想用 UnitTest 测试 test 方法。为了控制 MqttClient 实例,我想模拟该对象。我的测试-class 看起来如下:

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();

    }
}

当我执行测试时遇到以下错误:

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.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.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access[=12=]0(ParentRunner.java:53)
    at org.junit.runners.ParentRunner.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)

我可以在 field 上调用任何方法,它始终是空引用。我的问题是我不知道为什么会这样。

我将 field 的类型更改为其他几个 classes,包括 java.awt.pointSerializationInstantiatorHelper (in order to test if it has to do with MqttClient 作为外部依赖)以及我的项目中有几个 classes,在这些对象上调用适当的方法。在所有这些情况下,问题都不存在,测试成功,没有任何问题。模拟对象上的方法被调用但什么也不做——正如我想要的那样。

只有在使用MqttClient class时才会出现这个问题。或者至少我没有找到任何其他 class 重现这个问题。

老实说,此时我完全迷失了。是什么让 MqttClient class 如此特别?为什么我似乎不能像其他人一样嘲笑它 class?我错过了什么?

//编辑

我通过创建 MqttClient 的本地版本缩小了问题范围,复制了源代码。老实说,我现在更加困惑了。 class 具有三个具有以下签名的构造函数:

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

如果删除这三个构造函数中的一个,一切正常。 class 与这三个构造函数中的两个的任意组合都可以正常工作。 一旦所有三个构造函数都存在,就会出现上述错误消息。 这让我假设它可能与 PowerMockito 找不到正确的构造函数有关?如果是这样,那如何避免?

模拟目标似乎没有正确配置,因此在进行测试时它的行为不符合预期。

有时保持简单 (KISS) 是正确的方法。

@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();
    }
}

通过将 .withArguments 与适当的参数或匹配器一起使用,它有助于具体缩小为测试模拟哪个构造函数的范围,避免存在多个构造函数时发生任何冲突。

基于示例中显示的简化 class 测试,没有注入任何依赖项,因为它在本地初始化依赖项(即:紧耦合),因此实际上没有必要将模拟注入class 正在测试中。

此外 PrepareForTest 通常在测试 class 上完成,而不是在这种情况下的测试方法,根据 documentation

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

强调我的

我认为你运行和本期报道的问题一样

Mocking whenNew using withAnyArguments does not match null arguments #891

问题是如果声明了多个构造函数:

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).

因此,在问题得到解决之前,您可以使用变通方法:

MqttClient field = mock(MqttClient.class);

而不是@Mock MqttClient field;