在单元测试某些方法时,在某些情况下,某些参数的值无关紧要,可以是任何值。
例如,在这段代码中:
public void method(String arg1, String arg2, int arg3){
if(arg1 == null) throw new NullPointerException("arg1 is null");
//some other code
}
单元测试当arg1
是null
时必须抛出NPE的行为,其他参数的值无关紧要,它们可以是任何值,也可以是null
。
因此,我想记录这样一个事实,即值对测试中的方法并不重要。
我想到了以下选项:
选项1:定义ANY_XXX
的常量
我想过显式地创建常量ANY_STRING
和ANY_INT
,它们包含一个固定值,记录它可以是任何值,并且测试中的方法不关心实际值。
我可以将所有这些常量放在一个名为Any
的类中,并在所有测试类中重用它们。
选项2:ANY_XXX的随机值
这个选项对我来说似乎有点古怪,因为我在某个地方读到过不应该将随机性带入测试用例的内容。但在这种情况下,这种随机性将不可见,因为参数不会产生任何副作用。
哪种方法更适合更好、可读性更强的测试?
更新:
虽然我可以通过在Any
类中定义常量来使用ANY_XXX方法,但我也在考虑生成带有一些约束的ANY_XXX值,如
Any.anyInteger().nonnegative();
Any.anyInteger().negative();
Any.anyString().thatStartsWith("ab");
我在想,也许哈姆克雷斯特匹配器可以用来创建这种链接。但我不确定这种方法是否好。Mockito已经为anyObject()
提供了类似的方法,但这些方法只适用于Mocks和间谍,而不适用于普通对象。我想为更可读的测试实现普通对象的相同功能。
我为什么要这么做
假设我有一个类
class MyObject{
public MyObject(int param1, Object param2){
if(param1 < 0) throw new IllegalArgumentException();
if(param2 == null) throw new NullPointerException();
}
}
现在正在为构造函数编写测试
class MyObjectTest{
@Test(expected=NullPointerException.class)
public void testConstructor_ShouldThrowNullpointer_IfSecondParamIsNull(){
//emphasizing the fact that value of first parameter has no relationship with result, for better test readability
new MyObject(Any.anyInteger().nonnegative(), null);
}
}
我经常看到它们
就我个人而言,我不同意不应将随机性纳入测试。在某种程度上使用随机性应该会使你的测试更加稳健,但不一定更容易阅读
如果你采用第一种方法,我不会创建一个常量类,而是直接传递值(或null),因为这样你就可以看到你传递的内容,而不需要查看另一个类——这应该会让你的测试更可读。如果你稍后需要上的其他参数,你也可以很容易地修改你的测试
我的偏好是建立一个常量实用程序类以及帮助创建测试常数值的方法,例如:
public final class Values {
public static final int ANY_INT = randomInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
public static final int ANY_POSITIVE_INT = randomInt(0, Integer.MAX_VALUE);
public static final String ANY_ISBN = randomIsbn();
// etc...
public static int randomInt(int min, int max) { /* omitted */ }
public static String randomIsbn() { /* omitted */ }
// etc...
}
然后,我将使用静态导入来提取特定测试类所需的常量和方法。
我只在不关心值的情况下使用ANY_
常量,我发现它们可以使测试的意图更加清晰,例如:
// when
service.fooBar(ANY_INT, ANY_INT, ANY_INT, ANY_INT, 5);
很明显,值5
具有一定的意义——尽管它作为局部变量会更好。
在设置测试时,实用程序方法可用于临时生成值,例如:
// given
final String isbn1 = randomIsbn();
final String isbn2 = randomIsbn();
final Book[] books = { new Book(isbn1), new Book(isbn2) };
// when
bookRepository.store(books);
同样,这可以帮助测试类关注测试本身,而不是数据设置。
除此之外,我还从域对象中使用了类似的方法。当你将这两种方法结合起来时,它可能会非常强大。例如:
public final class Domain {
public static Book book() {
return new Book(randomIsbn());
}
// etc...
}
当我开始为我的项目编写单元测试时,我也遇到了同样的问题,并且必须处理大量的数组、列表、整数输入、字符串等。因此,我决定使用QuickCheck并创建一个生成器util类。
使用该库中的生成器,可以轻松生成基元数据类型和字符串。例如,当您想要生成一个整数时;只需使用IntegerGenerator类。您可以在生成器的构造函数中定义最大值和最小值。您还可以使用CombinedGeneratorSamples类生成列表、映射和数组等数据结构。这个库的另一个特性是为自定义类生成器实现生成器接口。
你想得太多了,给你的项目制造了不必要的障碍:
-
如果你想记录你的方法,可以用文字来记录!这就是为什么Javadoc在这里支持
-
如果你想用"任意正整数"测试你的方法,那么就用几个不同的正整数来调用它。在您的情况下,ANY并不意味着测试每一个可能的整数值
-
如果你想用"一个以ab开头的字符串"测试你的方法,用"abcd"调用它,然后用"abefgh",然后在测试方法上添加一个注释!
有时我们被框架和良好实践所困扰,以至于失去了常识。
最终:可读性最强=最简单
对实际方法使用调用方方法怎么样。
//This is the actual method that needs to be tested
public void theMethod(String arg1, String arg2, int arg3, float arg4 ){
}
创建一个调用方方法,该方法使用所需的参数和其他参数的默认(或null)值来调用该方法,并在此调用方方法上运行测试用例
//The caller method
@Test
public void invokeTheMethod(String param1){
theMethod(param1, "", 0, 0.0F); //Pass in some default values or even null
}
尽管您必须非常确信在theMethod(...)
上为其他参数传递默认值不会导致任何NPE。
我看到3个选项:
- 永远不要传零,禁止你的团队传零。null是邪恶的。传递null应该是一个例外,而不是规则
- 只需在生产代码中使用注释:@NotNull或类似的东西。如果使用lombok,这个注释也将进行实际验证
- 如果你真的必须在测试中这样做,那么只需创建一个具有适当名称的测试:
static final String ANY_STRING = "whatever";
@Test
public void should_throw_NPE_when_first_parameter_is_null() {
object.method(null, ANY_STRING, ANY_STRING); //use catch-exception or junit's expected
}
如果你愿意尝试JUnitParams的框架,你可以参数化你的测试,为你的参数指定有意义的名称:
@Test
@Parameters({
"17, M",
"2212312, M" })
public void shouldCreateMalePerson(int ageIsNotRelevant, String sex) throws Exception {
assertTrue(new Person(ageIsNotRelevant, sex).isMale());
}
我一直支持常量方法。原因是我相信它比链接几个匹配器更有可读性。
代替你的例子:
class MyObjectTest{
@Test(expected=NullPointerException.class)
public void testConstructor_ShouldThrowNullpointer_IfSecondParamIsNull(){
new MyObject(Any.anyInteger().nonnegative(), null);
}
}
我会d:
class MyObjectTest{
private static final int SOME_NON_NEGATIVE_INTEGER = 5;
@Test(expected=NullPointerException.class)
public void testConstructor_ShouldThrowNullpointer_IfSecondParamIsNull(){
new MyObject(SOME_NON_NEGATIVE_INTEGER, null);
}
}
此外,我更喜欢使用"SOME"而不是"ANY",但这也是个人品味的问题。
如果您正在考虑使用许多不同的变体(nonNegative()
、negative()
、thatStartsWith()
等)来测试构造函数,我建议您编写参数化测试。我推荐JUnitParams,以下是我的做法:
@RunWith(JUnitParamRunner.class)
class MyObjectTest {
@Test(expected = NullPointerException.class)
@Parameters({"-4000", "-1", "0", "1", "5", "10000"})
public void testConstructor_ShouldThrowNullpointer_IfSecondParamIsNull(int i){
new MyObject(i, null);
}
...
}
我建议您为那些可能是任意的参数使用常数值。添加随机性会使测试运行不可重复。即使参数值在这里"无关紧要",实际上唯一"有趣"的情况是测试失败,并且添加了随机行为,您可能无法轻松再现错误。此外,更简单的解决方案往往更好,也更容易维护:使用常数肯定比使用随机数更简单。
当然,如果你使用常量值,你可以把这些值放在static final
字段中,但你也可以把它们放在方法中,比如arbitraryInt()
(返回例如0)等等。我发现方法的语法比常量更干净,因为它类似于Mockito的any()
匹配器。它还允许您更容易地替换行为,以防以后需要添加更多复杂性
如果您想指示一个参数无关紧要,并且该参数是一个对象(而不是基元类型),那么您也可以传递空的mock,比如:someMethod(null, mock(MyClass.class))
。这向阅读代码的人传达了第二个参数可以是"任何东西",因为新创建的mock只有非常基本的行为。它也不会强迫您创建自己的方法来返回"任意"值。缺点是它不适用于基元类型或无法模拟的类,例如像String这样的最终类。
好的。。。。我看到一个大问题与你的方法!
其他值无关紧要?谁保证?测试的作者,代码的作者?如果你有一个方法,如果第一个参数正好是1000000,即使第二个参数是NULL,它也会抛出一些不相关的Exception,那该怎么办?
你必须制定你的测试用例:什么是测试规范。。。你想证明什么?是吗
-
在某些情况下,如果第一个参数是任意值,而第二个参数为null,则此方法应抛出NullPointerException
-
对于任何可能的第一个Input值,如果第二个值为NULL,则方法应始终抛出NullPointerException
如果你想测试第一种情况-你的方法是可以的。使用常量、随机值、生成器。。。不管你喜欢什么。
但是,如果你的规范实际上需要第二个条件,那么你提出的所有解决方案都不适合这项任务,因为它们只测试一些任意的值。如果程序员更改了方法中的一些代码,那么一个好的测试应该仍然有效。这意味着测试这种方法的正确方法是一系列测试用例,像测试所有其他方法一样测试所有角落的用例。因此,应该检查每个可能导致不同执行路径的关键值,或者你需要一个测试套件来检查代码路径的完整性。。。
否则你的测试就是假的,看起来很漂亮。。。