正确的单元测试技术
Proper unit testing technique
在使用 TDD 时,我发现自己需要测试一个包含查找值的常量(最终)哈希图(请查看更新中出现这种情况的原因)
见下文
private static final Map<Integer,String> singleDigitLookup = new HashMap<Integer, String>(){{
put(0,"Zero");put(1,"One");put(2,"Two");put(3,"Three");put(4,"Four");put(5,"Five");put(6,"Six");put(7,"Seven");
put(8,"Eight");put(9,"Nine");
}};
TDD 强调一次测试一件事,所以我开始调用我的 class 验证每个元素的有效性,如下所示。
TEST STYLE 1
@Test
public void whenWordIsOneThenReturn1(){
assertEquals(1, WordToIntegerConverter.toInteger("One"));
}
在写完第三个测试后,我觉得它很荒谬,并使用反向键值对创建了一个临时查找,并开始循环调用以进行如下测试。
TEST STYLE 2
@Test
public void whenWordIsZeroThroughNineReturnIntegerConversion(){
HashMap<Integer, String> lookup = new HashMap<Integer, String>(){{
put(0,"Zero");put(1,"One");put(2,"Two");put(3,"Three");put(4,"Four");put(5,"Five");
put(6,"Six");put(7,"Seven");put(8,"Eight");put(9,"Nine");
}};
for(int i = 0; i < 10; i++) {
assertEquals(i, WordToIntegerConverter.toInteger(lookup.get(i)));
}
}
我的问题是这样的;是使用样式 1 进行单元测试更好还是使用样式 2 更好。
我看到了两者的优点和缺点。例如样式1非常简洁,只测试一件事并且更容易理解。风格 1 的缺点除了做大量的输入之外,测试套件会因许多琐碎的测试而崩溃。样式 2 的优点是单元测试较少。样式 2 的缺点有点复杂,可能测试不止一件事,但我认为它只测试一件事常量哈希图的有效性。
更新
我从这个问题中收到了相当多的反馈,所以让我进一步解释一下。它本身不是我关心的常量,而是验证我的代码的不同情况。这是一个练习题(通过 Katas 练习 TDD)而不是生产代码。问题是将数字转换为单词,所以我在单元测试中关心的是确保我能够正确处理不同的可能数字。还有其他我没有包括的常量,例如常量存储青少年数字(11、12、13 ...)和 tensDigits(20、30、40 ...)。这里很容易打错字
方法 #1 完成了工作,只是 cut-n-pasting 的数量令人讨厌。方法 #2 解决了这个问题,但代价是测试不是独立的:如果一个测试未通过,则以下测试不会 运行。修复一个测试只是为了发现一堆新的测试现在失败是非常烦人的。您可以通过进行参数化测试来改进这一点,这里是 an example from junit's wiki:
@RunWith(Parameterized.class)
public class FibonacciTest {
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
});
}
private int fInput;
private int fExpected;
public FibonacciTest(int input, int expected) {
fInput= input;
fExpected= expected;
}
@Test
public void test() {
assertEquals(fExpected, Fibonacci.compute(fInput));
}
}
参数化测试包括 input/expected-output 对的集合,对于每一对,输入和输出都会传递到测试的构造函数调用中,并在新的测试实例上调用测试方法。循环保留在测试框架中和测试之外,每个测试的成功或失败独立于其他测试。
在使用 TDD 时,我发现自己需要测试一个包含查找值的常量(最终)哈希图(请查看更新中出现这种情况的原因)
见下文
private static final Map<Integer,String> singleDigitLookup = new HashMap<Integer, String>(){{
put(0,"Zero");put(1,"One");put(2,"Two");put(3,"Three");put(4,"Four");put(5,"Five");put(6,"Six");put(7,"Seven");
put(8,"Eight");put(9,"Nine");
}};
TDD 强调一次测试一件事,所以我开始调用我的 class 验证每个元素的有效性,如下所示。
TEST STYLE 1
@Test
public void whenWordIsOneThenReturn1(){
assertEquals(1, WordToIntegerConverter.toInteger("One"));
}
在写完第三个测试后,我觉得它很荒谬,并使用反向键值对创建了一个临时查找,并开始循环调用以进行如下测试。
TEST STYLE 2
@Test
public void whenWordIsZeroThroughNineReturnIntegerConversion(){
HashMap<Integer, String> lookup = new HashMap<Integer, String>(){{
put(0,"Zero");put(1,"One");put(2,"Two");put(3,"Three");put(4,"Four");put(5,"Five");
put(6,"Six");put(7,"Seven");put(8,"Eight");put(9,"Nine");
}};
for(int i = 0; i < 10; i++) {
assertEquals(i, WordToIntegerConverter.toInteger(lookup.get(i)));
}
}
我的问题是这样的;是使用样式 1 进行单元测试更好还是使用样式 2 更好。
我看到了两者的优点和缺点。例如样式1非常简洁,只测试一件事并且更容易理解。风格 1 的缺点除了做大量的输入之外,测试套件会因许多琐碎的测试而崩溃。样式 2 的优点是单元测试较少。样式 2 的缺点有点复杂,可能测试不止一件事,但我认为它只测试一件事常量哈希图的有效性。
更新 我从这个问题中收到了相当多的反馈,所以让我进一步解释一下。它本身不是我关心的常量,而是验证我的代码的不同情况。这是一个练习题(通过 Katas 练习 TDD)而不是生产代码。问题是将数字转换为单词,所以我在单元测试中关心的是确保我能够正确处理不同的可能数字。还有其他我没有包括的常量,例如常量存储青少年数字(11、12、13 ...)和 tensDigits(20、30、40 ...)。这里很容易打错字
方法 #1 完成了工作,只是 cut-n-pasting 的数量令人讨厌。方法 #2 解决了这个问题,但代价是测试不是独立的:如果一个测试未通过,则以下测试不会 运行。修复一个测试只是为了发现一堆新的测试现在失败是非常烦人的。您可以通过进行参数化测试来改进这一点,这里是 an example from junit's wiki:
@RunWith(Parameterized.class)
public class FibonacciTest {
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
});
}
private int fInput;
private int fExpected;
public FibonacciTest(int input, int expected) {
fInput= input;
fExpected= expected;
}
@Test
public void test() {
assertEquals(fExpected, Fibonacci.compute(fInput));
}
}
参数化测试包括 input/expected-output 对的集合,对于每一对,输入和输出都会传递到测试的构造函数调用中,并在新的测试实例上调用测试方法。循环保留在测试框架中和测试之外,每个测试的成功或失败独立于其他测试。