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

12
我有一个类似这样的方法:

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

我想编写一个单元测试,以确保当valuenullfoo()会抛出NPE,但是当IDE启用静态空指针流分析时,编译器拒绝编译单元测试。在启用“基于注释的空值分析”的情况下,如何使此测试编译通过(在Eclipse中):
@Test(expected = NullPointerException.class)
public void test() {
     T inst = ...
     inst.foo(null);
}

注意:理论上编译器的静态空指针应该防止这种情况发生。但是,没有任何东西能够阻止其他人编写一个关闭了静态流分析并使用null调用方法的模块。
常见情况:大而凌乱的旧项目没有流分析。我从注释一些实用模块开始。在这种情况下,我将拥有现有或新的单元测试,检查代码对于所有尚未使用流分析的模块的行为如何。
我的猜测是,我必须将这些测试移动到一个未经检查的模块中,并在扩展流分析时进行移动。这样做可以很好地适应哲学,但需要大量手工操作。
换句话说:我无法轻松地编写一个测试,以便在代码不编译时成功(我必须将代码片段放入文件中,在单元测试中调用编译器,检查输出是否有误...不太美观)。那么,如果调用者忽略@Nonnull,我如何轻松地测试代码是否会像应该失败?

@Nonnull 还是 @NotNull - Adam Arold
如果您传递了null变量会发生什么? String s = null; inst.foo(s); - StanislavL
3
感觉你试图测试的不是你的类,而是注释。 - daniu
1
似乎是针对IntelliJ的,但这可能会有所帮助 https://dev59.com/A1kR5IYBdhLWcg3wygDp#40847858 - Naman
你可以使用更复杂的情况 s = Boolean.TRUE ? null : "" 或者 s = "TRUE'.toLowerCase().equals("true") ? null : "" 来隐藏变量的初始化逻辑,但仍然拥有空变量。 - StanislavL
显示剩余7条评论
5个回答

8
在方法内隐藏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给出编译错误——因此“空值检查”已启用)。
与评论中给出的解决方案相比,上述方法具有良好的副作用,适用于任何类型的参数(但可能需要Java8才能始终正确推断类型)。
是的,测试通过(如上所述),并且在注释掉Objects.requireNonNull()行时失败。

这对于TestNG v7.1+不起作用。我看到:org.testng.TestException: Argument for @Nonnull parameter 'xyz' must not be null - kevinarpe
1
@kevinarpe 尝试创建一个没有值的静态 AtomicReference<Object>() 并返回它。 - Aaron Digulla

2
使用Jupiter断言中的assertThrows,我能够测试这个:
public MethodName(@NonNull final param1 dao) {....

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

1
当编译器启用了空值检查时,这段代码就不应该编译通过... - Aaron Digulla
只有在 assertThrows() 方法中调用该方法时,它才会对我进行编译。如果我正常调用该方法,它将无法编译。 - Sam Gruse
这对我来说看起来像是编译器的一个错误:它不应该特殊处理 assertThrows()。你能编译 Supplier<MethodName> foo = () -> new MethodName(null); 吗?Executable foo = () -> new MethodName(null); 呢? - Aaron Digulla

2
为什么不直接使用普通的反射技术?
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
}

请注意,在重构时(例如重命名方法,更改方法参数顺序,将方法移动到新类型中),此操作可能会意外破坏。

1
因为反射容易出问题? - GhostCat
1
但除此之外:我们可以再进一步——可能可以自动化更多的内容。意思是:如果“实例创建”遵循某些常见模式,那么可以扫描类路径以获取所有自定义类及其公共方法的注释。然后,您不仅可以通过反射调用该方法,还可以创建要调用的实例。 - GhostCat
这只是一个测试。测试会出错,然后你修复它们。显然,我不建议在生产代码中采用这种方法(除非有一个测试来验证它)。 - Sean Patrick Floyd

0

这里涉及到设计契约。您不能向带有notNull参数注释的方法提供空值参数。


嗯,如果你随机抛一枚硬币,在10%的情况下将null分配给一个字段...然后稍后将其用作该方法的参数,你认为任何编译时检查会告诉你这个问题吗? - GhostCat
2
而且更重要的是:就像现在你的输入一样,它更像是一条评论。一个“答案”应该回答问题。或者,在给出非答案时:非常清楚地解释为什么问题没有意义。 - GhostCat
如果您预期方法参数(而不是参数)可能为空,那么为什么要将API合同编写为notNull。 - LONGHORN007
我的理解是,我们在尝试测试的不是我们的类方法,而是注释。那么为什么我们要这样做呢? - LONGHORN007
再说一遍:读懂问题。他不是在测试注释,而是想测试公司内部合同,即使用此类注释的任何方法也必须检查参数是否为非空。 - GhostCat
显示剩余4条评论

0

您可以在设置方法中使用一个字段,将其初始化,然后设置为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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接