为@Nonnull 注释参数编写单元测试

Writing unit test for @Nonnull annotated parameter

我有一个这样的方法:

 public void foo(@Nonnull String value) {...}

我想编写单元测试以确保 foo()valuenull 时抛出 NPE 但我不能,因为编译器拒绝编译单元测试当在 IDE.

中启用静态空指针流分析时

如何编译此测试(在启用 "Enable annotation-based null analysis" 的 Eclipse 中):

@Test(expected = NullPointerException.class)
public void test() {
     T inst = ...
     inst.foo(null);
}

注意:理论上编译器的静态空指针应该可以防止这种情况发生。但是没有什么可以阻止某人在关闭静态流分析的情况下编写另一个模块并使用 null 调用该方法。

常见案例:没有流程分析的又大又乱的旧项目。我从注释一些实用程序模块开始。在那种情况下,我将进行现有的或新的单元测试,检查代码如何针对所有使用流分析尚未[=34=的模块运行].

我的猜测是,我必须将这些测试移动到一个未检查的模块中,并在我传播流分析时移动它们。这会奏效并且很符合理念,但需要大量的手动工作。

换句话说:我不能轻易地写出 "success when code doesn't compile" 的测试(我必须将代码片段放入文件中,从单元测试中调用编译器,检查输出是否有错误... 不漂亮)。那么,当调用者忽略 @Nonnull?

时,我如何才能 轻松地 测试代码失败呢?

此处按合同设计。您不能向使用 notNull 参数注释的方法提供 null 值参数。

null 隐藏在一个方法中就可以了:

public void foo(@NonNull String bar) {
    Objects.requireNonNull(bar);
}

/** Trick the Java flow analysis to allow passing <code>null</code>
 *  for @Nonnull parameters. 
 */
@SuppressWarnings("null")
public static <T> T giveNull() {
    return null;
}

@Test(expected = NullPointerException.class)
public void testFoo() {
    foo(giveNull());
}

上面的编译很好(是的,仔细检查 - 当使用 foo(null) 我的 IDE 给我一个编译错误 - 所以 "null checking" 被启用)。

与通过评论给出的解决方案相比,上面的方法对 any 类型的参数类型有很好的副作用(但可能需要 Java8让类型推断总是正确的)。

是的,测试通过(如上所述),但在注释掉 Objects.requireNonNull() 行时失败。

为什么不直接使用普通的旧反射?

try {
    YourClass.getMethod("foo", String.class).invoke(someInstance, null);
    fail("Expected InvocationException with nested NPE");
} catch(InvocationException e) {
    if (e.getCause() instanceof NullPointerException) {
        return; // success
    }
    throw e; // let the test fail
}

请注意,重构时这可能会意外中断(重命名方法、更改方法参数的顺序、将方法移动到新类型)。

您可以使用您初始化的字段,然后在设置方法中设置为 null

private String nullValue = ""; // set to null in clearNullValue()
@Before
public void clearNullValue() {
    nullValue = null;
}

@Test(expected = NullPointerException.class)
public void test() {
     T inst = ...
     inst.foo(nullValue);
}

正如 GhostCat 的回答一样,编译器无法知道 clearNullValue() 是否以及何时被调用,并且必须假设该字段不是 null.

使用 Jupiter 断言中的 assertThrows 我能够对此进行测试:

public MethodName(@NonNull final param1 dao) {....

assertThrows(IllegalArgumentException.class, () -> new MethodName(null));