你读过数学理论吗?
它看起来通常像这样:
对于所有的a,b>0,以下是正确的:a+b>a,a+b>b。
只是我们看到的定义通常难以理解。
譬如可以这样描述:它囊括了一个相当大的范围内(在此是无穷大)的所有元素(或者是元素的组合)。
与此相对应,一个典型的测试片段如下:
@Test
public void a_plus_b_is_greater_than_a_and_greater_than_b(){
int a = 2;
int b = 3;
assertTrue(a + b > a);
assertTrue(a + b > b);
}
这仅仅是对我们所谈论的大集合中的一个元素所进行的定义。不是很让人印象深刻。当然我们可以通过在测试上进行循环(或者使用参数化测试)来稍微休整一下这个问题。
@Test
public void a_plus_b_is_greater_than_a_and_greater_than_b_multiple_values() {
List<Integer> values = Arrays.asList(1, 2, 300, 400000);
for (Integer a : values)
for (Integer b : values) {
assertTrue(a + b > a);
assertTrue(a + b > b);
}
}
当然这仍然只测试了几个值,而且代码也看起来更难看了。我们竟然使用了9行代码来测试只写了一行的数学理论。而且最关键的是,在转化中应该对任意a,b值都适用的约束关系也完全消失了。
JUnit Theories带来了希望。让我们看一下使用这种强大的工具写出来的测试是什么样子的。
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertTrue;
@RunWith(Theories.class)
public class AdditionWithTheoriesTest {
@DataPoints
public static int[] positiveIntegers() {
return new int[]{
1, 10, 1234567};
}
@Theory
public void a_plus_b_is_greater_than_a_and_greater_than_b(Integer a, Integer b) {
assertTrue(a + b > a);
assertTrue(a + b > b);
}
}
使用JUnit Theories工具,测试被分成了两个部分:一个是提供数据点集(比如待测试的数据)的方法,另一个是理论本身。这个理论看起来几乎就像一个测试,但是它有一个不同的注解(@Theory),并且它需要参数。类通过使用数据点集的任意一种可能的组合来执行所有理论。
这意味着,如果我们有和测试主题相符的一个以上的理论,我们只需要声明一次数据点集。因此,让我们添加下面的理论,对加法来说应该是是正确的:a+b=b+a。所以我们将下面的理论添加至我们的类。
@Theory
public void addition_is_commutative(Integer a, Integer b) {
assertTrue(a + b == b + a);
}
这看起来很有魅力,你已经开始看到我们因为没有重复声明相同的数据点集,而少写了一部分代码。但我们仅仅对正整数进行了测试,而交换性是适用于所有整数的!当然我们的第一条理论仍然只对正数有效。
对此问题同样有相应的解决方案,那就是:Assume类。使用assume使得你可以在对理论测试前首先检查一下前提条件。如果条件不是一个正确的给定参数集,那么此理论将会跳过此参数集。所以我们的测试现在看起来像这样:
@RunWith(Theories.class)
public class AdditionWithTheoriesTest {
@DataPoints
public static int[] integers() {
return new int[]{
-1, -10, -1234567,1, 10, 1234567};
}
@Theory
public void a_plus_b_is_greater_than_a_and_greater_than_b(Integer a, Integer b) {
Assume.assumeTrue(a >0 && b > 0 );
assertTrue(a + b > a);
assertTrue(a + b > b);
}
@Theory
public void addition_is_commutative(Integer a, Integer b) {
assertTrue(a + b == b + a);
}
}
这使得测试进行了很好的表述。
除了简洁,由测试/理论模型实现的对测试数据进行的分离还有另外一点好处:你可能会开始考虑使你的测试数据独立于实际的东西来测试。
让我们开始这样做。如果你想要测试一个接受一个整数参数的方法,什么样的整数可能会造成问题呢?下面是我的建议:
@DataPoints
public static int[] integers() {
return new int[]{0, -1, -10, -1234567,1, 10, 1234567, Integer.MAX_VALUE, Integer.MIN_VALUE};}
这样测试我们的例子当然会失败了。如果你让Integer.MAX_VALUE加上一个正整数,将会得到一个溢出的值!所以我们了解到用当前形式所描述的理论是错误的!是的,这显而易见,但请再看看当前的项目。确实需要用MIN_VALUE,MAX_VALUE,0,正数和负数来进行所有使用整数的测试吗?是啊,确实应该如此。
那么更复杂的项目呢?字符串、日期、集合或者是域对象?使用JUnit Theories,你只需建立一次测试数据生成器,以用来创建所有更易产生问题的场景,然后在所有使用理论的测试中进行重用。这将会使你的测试更具表述力,也提高了发现错误的概率。