原文:https://github.com/junit-team/junit4/wiki
[TOC]
新版JUnit与旧版的差别
以前,如果用户想要使用JUnit进行单元测试,他们必须遵守以下规则:
- 创建一个用于测试的类,继承自junit.framework.TestCase;
- 保证所有用于测试的方法的名字以“test”为前缀;
- 利用一系列的assert方法进行测试。
下面是一个示例:
public class AdderTest extends TestCase {
public void testAdd() {
Adder Adder = new AdderImpl();
assertEquals(0, Adder.add(0, 0));
assertEquals(0, Adder.add(1, -1));
assertEquals(0, Adder.add(-1, 1));
assertEquals(2, Adder.add(1, 1));
assertEquals(-2, Adder.add(-1, -1));
}
}
现在,使用新版本的JUnit,可以摆脱以上的诸多限制,表现在:
- 不需要创建junit.framework.TestCase的子类,只需要把测试方法写在原类中就行;
- 测试方法不需要以test开头,只需要加上@Test注解。
下面是使用新版JUnit进行单元测试的例子:
public class AdderImpl implements Adder {
@Override
public int add(int a, int b) {
return a + b;
}
@Test
public void unitTest1(){
Adder Adder = new AdderImpl();
assertEquals(0, Adder.add(0, 0));
assertEquals(0, Adder.add(1, -1));
assertEquals(0, Adder.add(-1, 1));
assertEquals(2, Adder.add(1, 1));
assertEquals(-2, Adder.add(-1, -1));
}
}
断言
JUnit为所有的原始类型、对象以及数组提供了一系列重载的断言方法,参数的顺序统一为(expected,actual)(注:新版本中,针对float和double的assert方法加入了delta参数,代表期望值与实际值之间允许的最大差值,参数顺序为(expected,actual,delta))。在第一个参数之前还可以加上一个String来描述错误信息,它会在测试出错时被输出。这里有一个例外,assertThat方法接受用于描述错误信息的String,实际值actual以及一个Matcher对象。它的特殊之处在于实际值和期望值的顺序颠倒了。
下面是每种断言方法的使用示例:
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
public class AssertTests {
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertFalse() {
assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
assertSame("should be same", aNumber, aNumber);
}
// JUnit Matchers assertThat
@Test
public void testAssertThatBothContainsString() {
assertThat("albumen", both(containsString("a")).and(containsString("b")));
}
@Test
public void testAssertThatHasItems() {
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
}
@Test
public void testAssertThatEveryItemContainsString() {
assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
}
// Core Hamcrest Matchers with assertThat
@Test
public void testAssertThatHamcrestCoreMatchers() {
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
assertThat(new Object(), not(sameInstance(new Object())));
}
@Test
public void testAssertTrue() {
assertTrue("failure - should be true", true);
}
}
在Suite中组织多个测试
使用Suite运行器,你可以轻松地将多个类中的测试组织成一个组合测试。这和JUnit 3.8.x中的static Test suite()方法功能相同。想要使用Suite运行器,只需要在一个类上添加@RunWith(Suite.class)以及@SuiteClasses({TestClass1.class, …})两个注解即可。当你运行这个类,它会自动运行前面添加的所有测试。
接下来看一个示例。示例中的类仅用于放置各个注解,不需要实现任何方法。注意@RunWith注解,它指明了运行这组测试时要使用的测试运行器——org.junit.runners.Suite。此外,@SuiteClasses注解用来指明要测试的类以及测试时的先后顺序,它和@RunWith协同工作。
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestFeatureLogin.class,
TestFeatureLogout.class,
TestFeatureNavigate.class,
TestFeatureUpdate.class
})
public class FeatureTestSuite {
// the class remains empty,
// used only as a holder for the above annotations
}
测试执行顺序
JUnit设计时并没有明确指出测试方法的调用顺序。直到现在,这些方法都是根据反射API返回的顺序来调用的。然而,使用Java虚拟机给出的顺序是不明智的,因为Java平台也没有给出一个特别的顺序。实际上,JDK7返回的顺序差不多可以说是随机的。当然了,设计良好的测试用例不应当依赖任何执行顺序,但也有些测试可能会有这方面的需求。不管怎么说,一个可预测的失败总比一个依赖平台的随机发生的失败要好。
从4.1.1版本开始,JUnit默认使用一种确定的、非不可预测的顺序来调用测试方法(MethodSorters.DEFAULT)。如果想改变它,可以在测试类上加上@FixMethodOrder注解,并指定一种可用的MethodSorters。有下面几种:
@FixMethodOrder(MethodSorters.JVM):使用Java虚拟机返回的顺序。可能导致每次执行的顺序不同。
@FixMethodOrder(MethodSorters.NAME_ASCENDING):使用测试方法名的字典序。
下面是一个示例:
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestMethodOrder {
@Test
public void testA() {
System.out.println("first");
}
@Test
public void testB() {
System.out.println("second");
}
@Test
public void testC() {
System.out.println("third");
}
}
上面的代码会根据测试方法名的字典序的升序来调用它们。
异常测试
预期中的异常
要如何证明代码会和预期的一样抛出异常?确保代码会在特殊情况下抛出异常和确保程序能够正常运行完毕同样重要。比如:
new ArrayList<Object>().get(0);
这句代码应当会抛出一个IndexOutOfBoundsException。@Test注解有一个可选的参数“expected”,它接受Throwable的子类的Class对象。如果想要证明ArrayList会抛出正确的异常,应当这么写:
@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
new ArrayList<Object>().get(0);
}
expected参数需要被谨慎地使用。上面的测试只要有任何代码抛出IndexOutOfBoundsException就能通过。对于一些较长的测试,可以使用下文中的“ExpectedException rule”。
深度测试异常
上面的方法对于简单的测试类来说已经够用了。实际上,这种方法存在诸多限制,比如不能测试异常中包含的信息,以及异常抛出后某个域所处的状态。
在JUnit 3.x版本中,可以利用try-catch实现:
@Test
public void testExceptionMessage() {
try {
new ArrayList<Object>().get(0);
fail("Expected an IndexOutOfBoundsException to be thrown");
} catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
}
}
在JUnit4中,可以使用“ExpectedException rule”来替代上面的方案。这个rule不仅允许你指定期望出现的异常,还能够指定期望出现在异常中的信息:
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
List<Object> list = new ArrayList<Object>();
thrown.expect(IndexOutOfBoundsException.class);
thrown.expectMessage("Index: 0, Size: 0");
list.get(0); // execution will never get past this line
}
还可以使用Matchers来匹配异常中的信息,这种方式更加灵活:
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class TestExy {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void shouldThrow() {
TestThing testThing = new TestThing();
thrown.expect(NotFoundException.class);
thrown.expectMessage(startsWith("some Message"));
thrown.expect(hasProperty("response", hasProperty("status", is(404))));
testThing.chuck();
}
private class TestThing {
public void chuck() {
Response response = Response.status(Status.NOT_FOUND).entity("Resource not found").build();
throw new NotFoundException("some Message", response);
}
}
}
更多关于ExpectedException rule的使用可以参考http://baddotrobot.com/blog/2012/03/27/expecting-exception-with-junit-rule/index.html
Matchers与assertThat方法
Joe Walnes以后来的JMock 1为基础,构建了一种全新的断言方法——assertThat。它的句法如下所示:
assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));
更一般的形式为:
assertThat([value], [matcher statement]);
使用这种断言方法的优势如下:
- 可读性与可分性更强:这种句法让你能够按照主、谓、宾(x is 3)的顺序思考,而不是assertEquals使用的谓、宾、主的顺序(equals 3 x)。
- 易于组合:任何Matcher声明s都可以被否定(not(s))、组合(either(s).or(t))、映射成一个集合(each(s)),以及用在其他自定义的组合中。
- 可读的失败信息:比较下面两个示例:
assertTrue(responseString.contains("color") || responseString.contains("colour"));
// ==> failure message:
// java.lang.AssertionError:
assertThat(responseString, anyOf(containsString("color"), containsString("colour")));
// ==> failure message:
// java.lang.AssertionError:
// Expected: (a string containing "color" or a string containing "colour")
// got: "Please choose a font"
如果想要创建自定义的Matcher,只需要实现Matcher接口即可。
由于这种句法的可读性和扩展性都很强,我们决定在JUnit中直接包含这些API。
一些补充说明:
- 老的断言方法不会被抛弃,因为开发者可能会继续使用assertEquals、assertTrue等方法。
- 可以使用静态导入(import static)来更加方便地使用Matcher子句。我们希望所有的Java IDE都能添加对静态导入的支持。
- 为了允许Matcher间的互通,我们决定将这些来自Hamcrest项目的hamcrest-core的类加入到JUnit中。这也是JUnit首次加入第三方的类库;
- JUnit目前搬运了一小部分的Matcher,在org.hamcrest.CoreMatchers 与org.junit.matchers.JUnitMatchers中。(注:还有org.hamcrest.CoreMatchers)
- JUnit包含了对String和数组的特殊支持,能够提供关于它们为什么不同的信息。这在目前的assertThat方法中还未能得到实现。我们希望在将来发行的版本中能够让这两种断言方式的联系更加紧密。
忽略测试
出于某种原因,你不想让一个测试失败,你只是想暂时先忽略它。
想要忽略一个测试,你可以把那部分代码注释掉,或者删除@Test注解。不过,如果你这么做,测试运行器不会对这个测试作出任何响应。一种更好的方法是在@Test注解的前面或者后面加上@Ignore注解。测试运行器会对本次忽略的测试方法数量作出汇报。
@Ignore注解还支持一个可选的参数(一个String),用于说明忽略该测试的原因。
@Ignore("Test is ignored as a demonstration")
@Test
public void testSame() {
assertThat(1, is(1));
}
超时测试
如果测试花费的时间太长,可以让它自动地失败并停止。有两种可选的方法来实现这种行为:
(1)使用@Test注解的Timeout参数(仅对单个测试方法有效)
可以为单个测试方法设置超时时间,单位为毫秒。如果方法运行的时间超过了超时时间,它就会失败。当超时发生时,一个异常会被抛出,这个异常会引发失败。
@Test(timeout=1000)
public void testWithTimeout() {
...
}
上面的内容是通过将测试方法放在一个单独的线程中运行来实现的。如果测试运行超时,它会失败,并且JUnit会中断这个运行测试的线程。如果超时发生在执行一些可中断的操作时,这个线程就会退出。
(2)Timeout Rule(应用于测试类中所有测试方法)
Timeout Rule对测试类中所有的测试方法都设置了相同的超时时间,并且可以和@Test注解的Timeout参数协同工作(注:实际超时事件取较短的)。
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
public class HasGlobalTimeout {
public static String log;
private final CountDownLatch latch = new CountDownLatch(1);
@Rule
public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds max per method tested
@Test
public void testSleepForTooLong() throws Exception {
log += "ran1";
TimeUnit.SECONDS.sleep(100); // sleep for 100 seconds
}
@Test
public void testBlockForever() throws Exception {
log += "ran2";
latch.await(); // will block
}
}
Timeout Rule中设置的超时时间应用于整个测试过程,包括任何的@Before与@After方法。如果测试方法中存在无限循环(或者其他无法中断的代码),@After方法不会被调用。(注:测试下来还是调用了,可能是因为更新过了?)
参数化测试
Parameterized运行器提供了对参数化测试的支持。当运行一个参数化测试类时,测试实例是测试方法与测试数据的叉积。
下面是对一个斐波那契计算函数的测试示例:
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
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 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));
}
}
public class Fibonacci {
public static int compute(int n) {
int result = 0;
if (n <= 1) {
result = n;
} else {
result = compute(n - 1) + compute(n - 2);
}
return result;
}
}
测试用例会通过构造器以及有@Parameters注解的方法提供的数据自动生成。
注:@Parameters注解的方法(上面的示例中为data()方法返回一个Collection<Object[]>),里面每个Object[]中的每个元素会按照顺序传入构造器中。
利用@Parameter注解可以直接将数据值注入到对应的域中,免去了创建构造器的麻烦。像这样:
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@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 }
});
}
@Parameter // first data value (0) is default
public /* NOT private */ int fInput;
@Parameter(1)
public /* NOT private */ int fExpected;
@Test
public void test() {
assertEquals(fExpected, Fibonacci.compute(fInput));
}
}
public class Fibonacci {
...
}
这种方式目前只能用在public的域上。
(自 4.12-beta-3)如果你的测试只需要一个参数,那么就没必要将它包装在一个数组中,只需要提供一个Iterable或者一个数组即可。
@Parameters
public static Iterable<? extends Object> data() {
return Arrays.asList("first test", "second test");
}
或者:
@Parameters
public static Object[] data() {
return new Object[] { "first test", "second test" };
}
为了更加容易地识别参数化测试生成的每个测试用例,你可以利用@Parameters的可选参数name来为测试用例进行命名。name中允许包含下面两种占位符,它们会在运行时被自动替换:
- {index}:当前的参数索引(注:对应@Parameters标注的方法返回的集合)。
- {0}, {1}, …:各个参数值(注:对应集合中每个数组)。注意:单引号’应用双引号”转义。
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
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 FibonacciTest {
@Parameters(name = "{index}: fib({0})={1}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
});
}
private int input;
private int expected;
public FibonacciTest(int input, int expected) {
this.input = input;
this.expected = expected;
}
@Test
public void test() {
assertEquals(expected, Fibonacci.compute(input));
}
}
public class Fibonacci {
...
}
上面的示例中,Parameterized运行器会将测试用例命名为[1: fib(3)=2]这样的形式。默认使用当前参数集合的索引号来命名。
使用assume方法作假定
理想情况下,写测试的开发者能够掌控所有可能导致测试失败的因素。但在有些情况下这是几乎不可能的,此时就可以通过明确依赖来改善设计。
不过,这种做法未必是我们想要的,而且有时也做不到。理想的情况是能够拥有根据当前编写的代码、隐式的假设来运行测试,或是编写一个会暴露某个已知的Bug的能力。为了应对这些情形,JUnit提供了作假定的能力:
import static org.junit.Assume.*
@Test public void filenameIncludesUsername() {
assumeThat(File.separatorChar, is('/'));
assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
}
@Test public void correctBehaviorWhenFilenameIsNull() {
assumeTrue(bugFixed("13356")); // bugFixed is not included in JUnit
assertThat(parse(null), is(new NullDocument()));
}
默认的JUnit运行器会忽略失败的假定,自定义的运行器可能会有不同的行为。
为了方便,我们添加了assumeTrue方法。多亏有Hamcrest,我们免去了创建assumeEquals、assumeSame以及其他对应assert*的方法。所有这些方法都可以用assumeThat配上合适的Matcher实现。
规定(Rule)
Rule提供了一种非常灵活的方法,可以为所有测试用例提供额外的行为,或是重定义它们的某些行为。测试者可以重用或扩展下面提供的这些Rule,当然也可以编写自己的。
下面是一个使用了TemporaryFolder Rule以及ExpectedException Rule的例子:
public class DigitalAssetManagerTest {
@Rule
public final TemporaryFolder tempFolder = new TemporaryFolder();
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void countsAssets() throws IOException {
File icon = tempFolder.newFile("icon.png");
File assets = tempFolder.newFolder("assets");
createAssets(assets, 3);
DigitalAssetManager dam = new DigitalAssetManager(icon, assets);
assertEquals(3, dam.getAssetCount());
}
private void createAssets(File assets, int numberOfAssets) throws IOException {
for (int index = 0; index < numberOfAssets; index++) {
File asset = new File(assets, String.format("asset-%d.mpg", index));
Assert.assertTrue("Asset couldn't be created.", asset.createNewFile());
}
}
@Test
public void throwsIllegalArgumentExceptionIfIconIsNull() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Icon is null, not a file, or doesn't exist.");
new DigitalAssetManager(null, null);
}
}
下面是系统自带的几种基础Rule以及使用说明:
TemporaryFolder Rule
可以使用TemporaryFolder Rule创建文件和文件夹,它们会在测试方法结束后自动被删除(无论通不通过)。默认情况下,如果这些资源无法删除也不会抛出任何异常。
public static class HasTempFolder {
@Rule
public final TemporaryFolder folder = new TemporaryFolder();
@Test
public void testUsingTempFolder() throws IOException {
File createdFile = folder.newFile("myfile.txt");
File createdFolder = folder.newFolder("subfolder");
// ...
}
}
- 使用TemporaryFolder#newFolder(String… folderNames)能够递归地创建深层次的临时文件夹。
- TemporaryFolder#newFile()会创建一个随机名称的文件,#newFolder()会创建一个随机名称的文件夹。
- 从4.13版本起,TemporaryFolder支持一种可选的、更加严格的验证方式,会在无法删除创建的资源时用AssertionError来使测试失败。这种特性只能通过#builder()方法实现。默认情况下不会使用这种验证方式。
@Rule
public TemporaryFolder folder = TemporaryFolder.builder().assureDeletion().build();
ExternalResource Rules
ExternalResource是其他Rule的一个基类(和TemporaryFolder一样),它可以在每个测试开始前建立外部资源,并保证在测试后释放(注:就像@Before与@After的效果)。
public static class UsesExternalResource {
Server myServer = new Server();
@Rule
public final ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
myServer.connect();
};
@Override
protected void after() {
myServer.disconnect();
};
};
@Test
public void testFoo() {
new Client().run(myServer);
}
}
ErrorCollector Rule
ErrorCollector Rule允许测试在发现一个问题后继续执行。
public static class UsesErrorCollectorTwice {
@Rule
public final ErrorCollector collector = new ErrorCollector();
@Test
public void example() {
collector.addError(new Throwable("first thing went wrong"));
collector.addError(new Throwable("second thing went wrong"));
}
}
Verifier Rule
Verifier是ErrorCollector等Rule的基础,它能够在验证失败时让一个本能通过的测试失败。
private static String sequence;
public static class UsesVerifier {
@Rule
public final Verifier collector = new Verifier() {
@Override
protected void verify() {
sequence += "verify ";
}
};
@Test
public void example() {
sequence += "test ";
}
@Test
public void verifierRunsAfterTest() {
sequence = "";
assertThat(testResult(UsesVerifier.class), isSuccessful());
assertEquals("test verify ", sequence);
}
}
注:验证逻辑写在verify()中,如果验证不通过就抛出一个异常。
TestWatcher Rules
- 从4.9版本起,TestWatcher代替了TestWatchman。TestWatcher实现了TestRule而不是MethodRule。
- TestWatchman是4.7版本引进的,它使用一个MethodRule,现在已经被废弃了。
- TestWatcher(以及被废弃的TestWatchman)是那些具有观察测试行为而不对它进行修改的功能的Rule的基类。下面的类可以记录每个成功与失败的测试:
import static org.junit.Assert.fail;
import org.junit.AssumptionViolatedException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class WatchmanTest {
private static String watchedLog;
@Rule
public final TestRule watchman = new TestWatcher() {
@Override
public Statement apply(Statement base, Description description) {
return super.apply(base, description);
}
@Override
protected void succeeded(Description description) {
watchedLog += description.getDisplayName() + " " + "success!\n";
}
@Override
protected void failed(Throwable e, Description description) {
watchedLog += description.getDisplayName() + " " + e.getClass().getSimpleName() + "\n";
}
@Override
protected void skipped(AssumptionViolatedException e, Description description) {
watchedLog += description.getDisplayName() + " " + e.getClass().getSimpleName() + "\n";
}
@Override
protected void starting(Description description) {
super.starting(description);
}
@Override
protected void finished(Description description) {
super.finished(description);
}
};
@Test
public void fails() {
fail();
}
@Test
public void succeeds() {
}
}
Timeout Rule
Timeout Rule可以为测试类中所有测试方法设置相同的超时时间。
public static class HasGlobalTimeout {
public static String log;
@Rule
public final TestRule globalTimeout = Timeout.millis(20);
@Test
public void testInfiniteLoop1() {
log += "ran1";
for(;;) {}
}
@Test
public void testInfiniteLoop2() {
log += "ran2";
for(;;) {}
}
}
ExpectedException Rule
ExpectedException Rule能够测试抛出的异常以及异常中的信息。
public static class HasExpectedException {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void throwsNothing() {
}
@Test
public void throwsNullPointerException() {
thrown.expect(NullPointerException.class);
throw new NullPointerException();
}
@Test
public void throwsNullPointerExceptionWithMessage() {
thrown.expect(NullPointerException.class);
thrown.expectMessage("happened?");
thrown.expectMessage(startsWith("What"));
throw new NullPointerException("What happened?");
}
}
ClassRule
ClassRule扩展了方法级别的Rule,添加了一个static域来实现影响整个测试类的行为的目的(注:就像@BeforeClass与@AfterClass的效果)。任何ParentRunner的子类,包括标准的BlockJUnit4ClassRunner以及Suite都会支持ClassRule。
下面的例子中,在组合测试开始前会连接服务器,连接在测试结束后会自动断开。
@RunWith(Suite.class)
@SuiteClasses({A.class, B.class, C.class})
public class UsesExternalResource {
public static final Server myServer = new Server();
@ClassRule
public static final ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
myServer.connect();
};
@Override
protected void after() {
myServer.disconnect();
};
};
}
RuleChain
RuleChain能够组织一系列的TestRule的顺序:
public static class UseRuleChain {
@Rule
public final TestRule chain = RuleChain
.outerRule(new LoggingRule("outer rule"))
.around(new LoggingRule("middle rule"))
.around(new LoggingRule("inner rule"));
@Test
public void example() {
assertTrue(true);
}
}
上面的例子会输出这样的日志:
starting outer rule
starting middle rule
starting inner rule
finished inner rule
finished middle rule
finished outer rule
自定义Rule
绝大多数的Rule可以通过扩展ExternalResource实现。如果你需要更多的关于测试类和方法的信息,你需要实现TestRule接口:
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class IdentityRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
return base;
}
}
当然了,TestRule接口的威力来自于自定义构造器、添加用于测试中的方法、包装Statement等一系列步骤的组合。比如,你可能想要为每个测试提供一个日志文件:
package org.example.junit;
import java.util.logging.Logger;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class TestLogger implements TestRule {
private Logger logger;
public Logger getLogger() {
return this.logger;
}
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
logger = Logger.getLogger(description.getTestClass().getName() + '.' + description.getDisplayName());
base.evaluate();
}
};
}
}
可以这么使用这个Rule:
import java.util.logging.Logger;
import org.example.junit.TestLogger;
import org.junit.Rule;
import org.junit.Test;
public class MyLoggerTest {
@Rule
public final TestLogger logger = new TestLogger();
@Test
public void checkOutMyLogger() {
final Logger log = logger.getLogger();
log.warn("Your test is showing!");
}
}
(理论)Theories
更加灵活、表达力更强的断言(assertions),和假定(assumptions)引出了一种新型的目的(intent),我们把它叫做“理论(theories)”。一次测试只能捕捉到对象在一个特定的场景下的行为,而理论可以通过无限数量的场景来捕捉到对象行为的一些其他方面。(原文:More flexible and expressive assertions, combined with the ability to state assumptions clearly, lead to a new kind of statement of intent, which we call a “Theory”. A test captures the intended behavior in one particular scenario. A theory captures some aspect of the intended behavior in possibly infinite numbers of potential scenarios.)
@RunWith(Theories.class)
public class UserTest {
@DataPoint
public static String GOOD_USERNAME = "optimus";
@DataPoint
public static String USERNAME_WITH_SLASH = "optimus/prime";
@Theory
public void filenameIncludesUsername(String username) {
assumeThat(username, not(containsString("/")));
assertThat(new User(username).configFileName(), containsString(username));
}
}
很显然,用户名应当被包含在配置文件名中,不应当包含’/’。另一个测试或理论可能会定义如果用户名包含’/’会怎么样。
UserTest会尝试在类中定义的每个兼容的DataPoint上运行filenameIncludesUsername。如果某个假定失败了,这个DataPoint就会被默默地忽略。如果所有的假定都通过了,而有一个断言失败了,这个测试会失败。
JUnit对理论的支持是从Popper中吸收来的,这里有一份更加完备的文档:http://web.archive.org/web/20071012143326/popper.tigris.org/tutorial.html
以及一篇论文:http://web.archive.org/web/20110608210825/http://shareandenjoy.saff.net/tdd-specifications.pdf
来自Popper的文档(注:翻译有删改)
注:下面用到的被测试类Dollar:
public class Dollar{
private int amount;
public Dollar(int amount) {
this.amount = amount;
}
public Dollar times(int m){
amount *= m;
return this;
}
public Dollar divideBy(int m){
amount /= m;
return this;
}
public int getAmount(){
return 10;
}
}
让我们以一个简单的测试类开始,它用于检验关于Dollar的一些算术操作:
public class DollarTest {
@Test
public void multiplyByAnInteger() {
assertThat(new Dollar(5).times(2).getAmount(), is(10));
}
}
上面的测试显然可以通过。在这里,我特别建议你让getAmount()方法永远返回0来欺骗测试类。
接下来看看Theory是怎么工作的。对测试类进行以下改动:
@RunWith(Theories.class)
public class DollarTest {
@Test
public void multiplyByAnInteger() {
assertThat(new Dollar(5).times(2).getAmount(), is(10));
}
@Theory
public void multiplyIsInverseOfDivide(int amount, int m) {
assumeThat(m, not(0));
assertThat(new Dollar(amount).times(m).divideBy(m).getAmount(), is(amount));
}
}
运行一下会发现,multiplyByAnInteger测试方法依旧能通过,但multiplyIsInverseOfDivide会失败,错误信息为“Never found parameters that satisfied method.”这是因为运行器不会自动为理论提供数据。可以使用@DataPoint来设置数据(必须为static的):
@DataPoint
public static int TWO = 2;
@DataPoint
public static int FIVE = 5;
@DataPoint
public static int TEN = 10;
由于multiplyIsInverseOfDivide需要两个参数,根据三个数据点,一共会生成9种测试用例(3*3)。
也可以使用@TestedOn注解:
@Theory
public void multiplyIsInverseOfDivide(
@TestedOn(ints = {0,5,10})int amount,
@TestedOn(ints = {0,1,2})int m) {
assumeThat(m, not(0));
assertThat(new Dollar(amount).times(m).divideBy(m).getAmount(), is(amount));
}
同样会生成(0,5,10)×(0,1,2)的共9种组合。需要注意的是:尽管0被作为m的一个候选值,它也没有引发ArithmeticException:违反假定的测试用例直接会被忽略。
你可以实现自己的parameter supplier,像下面的例子这样:
@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(BetweenSupplier.class)
public @interface Between {
int first();
int last();
}
public static class BetweenSupplier extends ParameterSupplier {
@Override
public List getValues(Object test, ParameterSignature sig) {
Between annotation = (Between) sig.getSupplierAnnotation();
ArrayList list = new ArrayList();
for (int i = annotation.first(); i <= annotation.last(); i++)
list.add(i);
return list;
}
}
它可以生成一个范围内的数字,只需要这么使用:
@Theory
public void multiplyIsInverseOfDivideWithInlineDataPoints(
@Between(first = -100, last = 100) int amount,
@Between(first = -100, last = 100) int m
) {
assumeThat(m, not(0));
assertThat(new Dollar(amount).times(m).divideBy(m).getAmount(), is(amount));
}
测试固件
测试固件由一系列的对象组成,用作运行测试的基准线。之所以使用测试固件,是因为许多的测试都需要确保在一个已知的、固定的环境下进行。比如:
- 准备输入数据以及创建模拟对象;
- 加载数据库;
- 复制一批文件;
JUnit提供了一些注解,可以用来标注那些需要在每次测试前后运行的,或者在整个测试开始前后运行的方法。
有以下四种注解,两个是类级别的(@BeforeClass、@AfterClass),两个是方法级别的(@Before、@After)。
利用Rule也可以实现这种效果,前文有提到。
示例:
package test;
import java.io.Closeable;
import java.io.IOException;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class TestFixturesExample {
static class ExpensiveManagedResource implements Closeable {
@Override
public void close() throws IOException {}
}
static class ManagedResource implements Closeable {
@Override
public void close() throws IOException {}
}
@BeforeClass
public static void setUpClass() {
System.out.println("@BeforeClass setUpClass");
myExpensiveManagedResource = new ExpensiveManagedResource();
}
@AfterClass
public static void tearDownClass() throws IOException {
System.out.println("@AfterClass tearDownClass");
myExpensiveManagedResource.close();
myExpensiveManagedResource = null;
}
private ManagedResource myManagedResource;
private static ExpensiveManagedResource myExpensiveManagedResource;
private void println(String string) {
System.out.println(string);
}
@Before
public void setUp() {
this.println("@Before setUp");
this.myManagedResource = new ManagedResource();
}
@After
public void tearDown() throws IOException {
this.println("@After tearDown");
this.myManagedResource.close();
this.myManagedResource = null;
}
@Test
public void test1() {
this.println("@Test test1()");
}
@Test
public void test2() {
this.println("@Test test2()");
}
}
运行测试后会输出:
@BeforeClass setUpClass
@Before setUp
@Test test2()
@After tearDown
@Before setUp
@Test test1()
@After tearDown
@AfterClass tearDownClass
类别
通过@IncludeCategory注解,开发者可以对测试类和测试方法指定类别。类别运行器可以选择只运行指定类别或是指定类别的子类的测试类和测试方法。类和接口都可以用作类别。例如,如果使用@IncludeCategory(SuperClass.class),那么只会运行带有@Category({SubClass.class})注解的测试。
也可以使用@ExcludeCategory注解来排除某些类别。
示例:
public interface FastTests { /* category marker */ }
public interface SlowTests { /* category marker */ }
public class A {
@Test
public void a() {
fail();
}
@Category(SlowTests.class)
@Test
public void b() {
}
}
@Category({SlowTests.class, FastTests.class})
public class B {
@Test
public void c() {
}
}
@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@SuiteClasses( { A.class, B.class }) // Note that Categories is a kind of Suite
public class SlowTestSuite {
// Will run A.b and B.c, but not A.a
}
@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@ExcludeCategory(FastTests.class)
@SuiteClasses( { A.class, B.class }) // Note that Categories is a kind of Suite
public class SlowTestSuite {
// Will run A.b, but not A.a or B.c
}
在Maven中使用JUnit
添加如下依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
在Gradle中使用JUnit
确保build.gradle中有以下内容:
apply plugin: 'java'
dependencies {
testCompile 'junit:junit:4.12'
}