Jade Dungeon

JUnit

JUnit Theories

你读过数学理论吗?它看起来通常像这样:

对于所有的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行代码来测试 只写了一行的数学理论。而且最关键的是,在转化中应该对任意ab值都适用的 约束关系也完全消失了。

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_VALUEMAX_VALUE0 ,正数和负数来进行所有使用整数的测试吗?是啊,确实应该如此。

那么更复杂的项目呢?字符串、日期、集合或者是域对象?使用JUnit Theories,你只需 建立一次测试数据生成器,以用来创建所有更易产生问题的场景,然后在所有使用理论的 测试中进行重用。这将会使你的测试更具表述力,也提高了发现错误的概率。

JUnit 注解与静态方法

常用注解

  • @Test (expected = Exception.class)表示预期会抛出Exception.class 的异常
  • @Ignore含义是「某些方法尚未完成,暂不参与此次测试」。 这样的话测试结果就会提示你有几个测试被忽略,而不是失败。 一旦你完成了相应函数,只需要把@Ignore注解删去,就可以进行正常的测试。
  • @Test(timeout=100)表示预期方法执行不会超过 100 毫秒,控制死循环
  • @Before表示该方法在每一个测试方法之前运行,可以使用该方法进行初始化之类的操作
  • @After表示该方法在每一个测试方法之后运行,可以使用该方法进行释放资源,回收内存之类的操
  • @BeforeClass表示该方法只执行一次,并且在所有方法之前执行。 一般可以使用该方法进行数据库连接操作,注意该注解运用在静态方法。
  • @AfterClass表示该方法只执行一次,并且在所有方法之后执行。 一般可以使用该方法进行数据库连接关闭操作,注意该注解运用在静态方法。

静态类junit.framework.Assert

该类主要包含七个方法:

  • assertEquals()方法,用来查看对象中存的值是否是期待的值, 与字符串比较中使用的equals()方法类似;
  • assertFalse()assertTrue()方法,用来查看变量是是否为falsetrue, 如果assertFalse()查看的变量的值是false则测试成功,如果是true则失败, assertTrue()与之相反。
  • assertSame()assertNotSame()方法,用来比较两个对象的引用是否相等和不相等, 类似于通过==!=比较两个对象;
  • assertNull()assertNotNull()方法,用来查看对象是否为空和不为空。

TestSuite

如果你须有多个测试单元,可以合并成一个测试套件进行测试,况且在一个项目中, 只写一个测试类是不可能的,我们会写出很多很多个测试类。 可是这些测试类必须一个一个的执行,也是比较麻烦的事情。

鉴于此, JUnit 为我们提供了打包测试的功能,将所有需要运行的测试类集中起来, 一次性的运行完毕,大大的方便了我们的测试工作。 并且可以按照指定的顺序执行所有的测试类。

下面的代码示例创建了一个测试套件来执行两个测试单元。 如果你要添加其他的测试单元可以使用语句@Suite.SuiteClasses进行注解。

import org.junit.runner.RunWith;  
import org.junit.runners.Suite;  
import org.junit.runners.Suite.SuiteClasses;  
  
@RunWith( Suite.class )  
@SuiteClasses( { JUnit1Test.class, StringUtilTest.class } )  
public class JSuit {  
}   

TestSuite 测试包类——多个测试的组合 TestSuite 类负责组装多个 Test Cases。 待测得类中可能包括了对被测类的多个测试,而 TestSuit 负责收集这些测试, 使我们可以在一个测试中,完成全部的对被测类的多个测试。

TestSuite 类实现了 Test 接口,且可以包含其它的 TestSuites。 它可以处理加入Test 时的所有抛出的异常。

TestResult 结果类集合了任意测试累加结果,通过 TestResult 实例传递个每个测试的 Run()方法。

TestResult 在执行 TestCase 是如果失败会异常抛出 TestListener 接口是个事件监听规约, 可供 TestRunner 类使用。它通知 listener 的对象相关事件, 方法包括:

  • 测试开始startTest(Test test)
  • 测试结束endTest(Test test)
  • 错误,增加异常addError(Test test, Throwable t)增加失败addFailure(Test test, AssertionFailedError t)

TestFailure 失败类是个「失败」状况的收集类,解释每次测试执行过程中出现的异常情况, 其toString()方法返回「失败」状况的简要描述。

  • JUnit 可以指定 Runner 运行器
  • JUnit 可以参数化测试
  • 事实上在Junit 中使用try-catch 来捕获异常是没有必要的,Junit 会自动捕获异常。那些没有被捕获的异常就被当成错误处理
  • 不要认为压力大,就不写测试代码。相反编写测试代码会使你的压力逐渐减轻,因为通过编写测试代码,你对类的行为有了确切的认识。你会更快地编写出有效率地工作代码。

参数化测试

对同一个方法,不同的参数测试不同的分支:

public class MyObj {

	public boolean myFunc(int n) {
		return n > 0 ? true : false;
	}

}

指定特殊的运行器org.junit.runners.Parameterized, 构造函数完成对输入参数和期待结果变量的赋值:

import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class ExampleParamTest {

	private int inputVal; // 输入参数
	private boolean expectRst; // 期待的结果

	public ExampleParamTest(int inputVal, boolean expectRst) {
		this.inputVal = inputVal;
		this.expectRst = expectRst;
	}
	...
}

为测试类声明一个注解@Parameters,返回值为Collection的公共静态方法:

@Parameters
public static Collection<Object[]> prepareData() {
	/* 并初始化所有需要测试的参数对。 */
	Object[][] object = { { -1, false }, { 13, true } };
	return Arrays.asList(object);
}

调用每个测试方法前都要创建一个新的测试目标对象:

private MyObj myObj; // 要调试的类

@Before
public void setUp() throws Exception {
	this.myObj = new MyObj();
}

@Test
public void test() {
	System.out.println(expectRst + ",  " + inputVal);
	assertEquals(expectRst, myObj.myFunc(inputVal));
}

完整的例子:

import static org.junit.Assert.assertEquals;

import java.util.Arrays;
import java.util.Collection;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class ExampleParamTest {

	private int inputVal; // 输入参数
	private boolean expectRst; // 期待的结果

	public ExampleParamTest(int inputVal, boolean expectRst) {
		this.inputVal = inputVal;
		this.expectRst = expectRst;
	}

	private MyObj myObj; // 要调试的类

	@Before
	public void setUp() throws Exception {
		this.myObj = new MyObj();
	}

	@Parameters
	public static Collection<Object[]> prepareData() {
		Object[][] object = { { -1, false }, { 13, true } };
		return Arrays.asList(object);
	}

	@Test
	public void test() {
		System.out.println(expectRst + ",  " + inputVal);
		assertEquals(expectRst, myObj.myFunc(inputVal));
	}

}

class MyObj {

	public boolean myFunc(int n) {
		return n > 0 ? true : false;
	}

}