Post

JUnit使用教程

1.简介

JUnit是一个Java单元测试框架,用于编写和运行可重复的自动化测试。

2.快速入门

https://github.com/junit-team/junit4/wiki/Getting-started

下面的示例展示了如何使用JUnit编写并运行测试。

2.1 准备工作

首先需要安装JDK和Maven。创建一个新目录junit-demo,下载junit-4.13.2.jarhamcrest-core-1.3.jar到这个目录中。

2.2 编写被测类

在junit-demo目录下创建一个新文件Calculator.java,内容如下:

1
2
3
4
5
6
7
8
public class Calculator {
    public int evaluate(String expression) {
        int sum = 0;
        for (String summand : expression.split("\\+"))
            sum += Integer.parseInt(summand);
        return sum;
    }
}

编译这个类:

1
javac Calculator.java

得到文件Calculator.class。

2.3 编写测试

创建文件CalculatorTest.java,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class CalculatorTest {
    @Test
    public void evaluatesExpression() {
        Calculator calculator = new Calculator();
        int sum = calculator.evaluate("1+2+3");
        assertEquals(6, sum);
    }
}

@Test注解标记的方法是一个测试用例(test case),该方法必须是public void

编译这个测试类:

1
javac -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar CalculatorTest.java

得到文件CalculatorTest.class。

注意,在Windows上需要将类路径分隔符改为分号:

1
javac -cp .;junit-4.13.2.jar;hamcrest-core-1.3.jar CalculatorTest.java

目录结构如下:

1
2
3
4
5
6
7
junit-demo/
    Calculator.java
    Calculator.class
    CalculatorTest.java
    CalculatorTest.class
    junit-4.13.2.jar
    hamcrest-core-1.3.jar

2.4 运行测试

从命令行运行测试:

1
2
3
4
5
6
$ java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore CalculatorTest
JUnit version 4.13.2
.
Time: 0.002

OK (1 test)

在输出中, “.” 表示运行了一个测试, “OK” 表示测试通过。

2.5 让测试失败

修改Calculator.java,将sum += Integer.parseInt(summand);改为sum -= Integer.parseInt(summand);,并重新编译Calculator类。再次运行测试,测试将会失败并得到以下输出:

1
2
3
4
5
6
7
8
9
10
11
JUnit version 4.13.2
.E
Time: 0.002
There was 1 failure:
1) evaluatesExpression(CalculatorTest)
java.lang.AssertionError: expected:<6> but was:<-6>
        at org.junit.Assert.fail(Assert.java:89)
        ...

FAILURES!!!
Tests run: 1,  Failures: 1

JUnit给出了失败的测试以及失败原因。

2.6 使用Maven

创建Maven项目,添加Maven依赖:

1
2
3
4
5
6
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
junit-demo/
    pom.xml
    src/
        main/
            java/
                com/example/
                    Calculator.java
        test/
            java/
                com/example/
                    CalculatorTest.java

在项目根目录下执行

1
mvn test

完整代码:https://github.com/ZZy979/junit-demo/

如果使用Gradle,参见Use with Gradle

2.7 使用IDE

很多IDE都提供了JUnit支持。以IntelliJ IDEA为例,可以直接点击测试类或测试方法左侧的按钮运行测试,如下图所示:

在IntelliJ IDEA中使用JUnit

3.断言

断言(assertion)是定义在org.junit.Assert类中的静态方法,用于验证结果(例如比较两个值是否相等)。当断言失败时,JUnit将抛出AssertionError异常,并打印失败的测试和错误信息。

JUnit为所有基本类型、对象以及数组提供了重载断言方法。大多数断言方法有两个参数:第一个为预期值,第二个为实际值。另外,这些方法都有三个参数的版本,第一个参数是失败时输出的字符串消息(可选)。

常用的断言方法如下表所示:

断言验证条件
assertTrue(condition)condition为真
assertFalse(condition)condition为假
assertEquals(expected, actual)expected等于actual(对于浮点数需要指定第三个参数delta
assertNotEquals(unexpected, actual)unexpected不等于actual(对于浮点数需要指定第三个参数delta
assertArrayEquals(expected, actual)数组expectedactual相等
assertNull(obj)obj == null
assertNotNull(obj)obj != null
assertSame(expected, actual)expected == actual
assertNotSame(expected, actual)expected != actual
assertThrows(expected, runnable)runnable.run()抛出expected类型的异常
assertThat(actual, matcher)actual满足匹配器matcher

其中,assertThat()的第二个参数是匹配器,详见“匹配器和assertThat”一节。

完整列表参考:https://junit.org/junit4/javadoc/latest/org/junit/Assert.html

断言和匹配器示例AssertTests

4.测试运行器

测试运行器(test runner)是用于运行测试并显示结果的类。

4.1 控制台运行器

JUnit提供了从控制台运行测试的运行器JUnitCore(已经在2.4节使用过)。可以在命令行中执行:

1
java org.junit.runner.JUnitCore TestClass1 TestClass2 ...

也可以在代码中调用runClasses()方法:

1
JUnitCore.runClasses(TestClass1.class, TestClass2.class, ...);

详见文档:https://junit.org/junit4/javadoc/latest/org/junit/runner/JUnitCore.html

4.2 @RunWith注解

如果一个测试类带有注解@RunWith,或者继承了带有该注解的类,JUnit就会使用指定的运行器来运行这个类中的测试。默认的运行器是BlockJUnit4ClassRunner

4.3 专用运行器

5.测试套件

测试套件(test suite)是测试类的集合。使用Suite运行器可以手动构建测试套件,包含来自多个类的测试。为此,创建一个空的类,并添加注解@RunWith(Suite.class)@SuiteClasses({TestClass1.class, ...})。当运行这个类时,JUnit将运行套件包含的所有测试。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}

6.测试执行顺序

默认情况下,JUnit不指定测试方法的执行顺序。良好的测试代码不应该依赖于执行顺序,但有时测试方法之间存在依赖关系。本节介绍如何改变测试方法的执行顺序。

6.1 @OrderWith注解

从4.13版本开始,可以使用@OrderWith注解指定测试方法的执行顺序,其参数是一个Ordering.Factory的实例,用于提供重排序测试的Ordering实现。

JUnit提供了Alphanumeric,用于根据测试方法名排序:@OrderWith(Alphanumeric.class)

测试执行顺序示例TestMethodOrder

6.2 @FixMethodOrder注解

从4.11版本开始,可以使用@FixMethodOrder注解改变测试执行顺序,并指定一个MethodSorters

  • MethodSorters.JVM:按照JVM(反射接口)返回的顺序排序
  • MethodSorters.NAME_ASCENDING:按照方法名字典序排序
  • MethodSorters.DEFAULT(默认):按照一种确定的但不可预测的顺序排序

7.异常测试

在JUnit中有多种方式可以编写测试来验证代码按预期抛出异常。

7.1 使用assertThrows方法

在4.13版本中,Assert类添加了assertThrows()方法。使用该方法可以断言给定的函数调用(例如lambda表达式或方法引用)会抛出特定类型的异常。该方法还会返回抛出的异常,以便进行进一步的断言(例如验证错误信息)。此外,还可以在抛出异常之后对域对象(被测对象)的状态做断言。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testExceptionAndState() {
    List<Object> list = new ArrayList<>();

    IndexOutOfBoundsException thrown = assertThrows(
            IndexOutOfBoundsException.class,
            () -> list.add(1, new Object()));

    // assertions on the thrown exception
    assertEquals("Index: 1, Size: 0", thrown.getMessage());
    // assertions on the state of a domain object after the exception has been thrown
    assertTrue(list.isEmpty());
}

7.2 使用try/catch

如果使用JUnit 4.13以下版本,可以使用“try/catch习语”:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testExceptionMessage() {
    List<Object> list = new ArrayList<>();

    try {
        list.get(0);
        fail("Expected an IndexOutOfBoundsException to be thrown");
    } catch (IndexOutOfBoundsException e) {
        assertEquals("Index: 0, Size: 0", e.getMessage());
    }
}

注意,fail()会抛出AssertionError,因此不能使用这种方式来验证应该抛出AssertionError的方法。

7.3 使用@Test注解

@Test注解有一个可选的expected参数,可以指定异常类型。

1
2
3
4
@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
    new ArrayList<Object>().get(0);
}

注意,方法中的任何代码抛出IndexOutOfBoundsException,上述测试都会通过(例如,假设ArrayList构造器抛出该异常,测试也会通过)。另外,使用这种方式无法验证异常的错误信息,也无法在抛出异常之后验证被测对象的状态。因此不推荐这种方式。

7.4 ExpectedException规则

测试异常的另一种方式是ExpectedException规则,但这种方式在JUnit 4.13中已被弃用。

1
2
3
4
5
6
7
8
9
10
11
@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void shouldTestExceptionMessage() {
    List<Object> list = new ArrayList<>();

    thrown.expect(IndexOutOfBoundsException.class);
    thrown.expectMessage("Index: 0, Size: 0");
    list.get(0);
}

expectMessage()还允许使用匹配器,使测试更加灵活。例如:

1
thrown.expectMessage(containsString("Size: 0"));

注意,当测试调用抛出异常的方法时,后面的代码就不会执行。

另见Expecting Exceptions JUnit Rule

完整代码:异常测试示例ExceptionTest

8.匹配器和assertThat

8.1 assertThat

断言assertThat()用于验证给定值满足匹配器指定的条件,语法如下:

1
assertThat(actual, matcher);

例如:

1
2
3
4
assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));

这种断言语法的优点包括:

  • 更易读、更易于输入:这种语法按“主-谓-宾”的顺序书写(assertThat(x, is(3)) assert “x is 3”),而不是“谓-宾-主”(assertEquals(3, x) assert “equals 3 x”)。
  • 组合:任何匹配器s都可以取反(not(s))、组合(either(s).or(t))、映射到集合(each(s))或用于自定义组合(afterFiveSeconds(s))。
  • 易读的错误消息:
1
2
3
4
5
6
7
8
9
10
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接口,可以为自定义断言获得上述优点。

详见Flexible JUnit assertions with assertThat()

8.2 匹配器

匹配器(matcher)是实现了org.hamcrest.Matcher接口的对象,用于验证一个值是否满足特定的条件。

注意:自定义匹配器不应该直接实现Matcher接口,而应该扩展BaseMatcher抽象类。

JUnit以静态工厂方法的形式提供了一些常用的匹配器:

可以用静态导入包含需要的匹配器:

1
import static org.hamcrest.CoreMatchers.*;

JUnit依赖Hamcrest,Maven等构建工具会自动添加依赖库。

第三方匹配器:

9.忽略测试

要忽略一个测试,可以将其注释掉,或者删除@Test注解。另外,也可以添加@Ignore注解,运行器将报告忽略的测试数量。

@Ignore接受一个可选的字符串参数,可以指定忽略的原因。例如:

1
2
3
4
5
@Ignore("Test is ignored as a demonstration")
@Test
public void testSame() {
    assertThat(1, is(1));
}

@Ignore注解也可用于测试类。

10.测试超时

如果希望耗时过长的测试自动失败,有两种方式可以实现这一行为。

10.1 @Test注解的timeout参数

可以指定@Test注解的timeout参数(单位为毫秒),应用于单个测试方法。如果超过时间限制,测试将失败。

1
2
3
4
@Test(timeout = 1000)
public void testWithTimeout() {
    ...
}

10.2 Timeout规则

Timeout规则应用于测试类中的所有测试方法。

测试超时示例HasGlobalTimeout

Timeout规则指定的超时时间应用于整个测试类,包括@Before@After方法。如果测试方法是一个无限循环(或者无法中断),则@After方法不会被调用。

11.参数化测试

11.1 构造器注入

Parameterized运行器用于实现参数化测试。运行参数化测试类时,将为测试方法和测试数据元素的叉乘(笛卡尔积)分别创建实例。返回测试数据的方法使用@Parameters注解标记。

例如,要测试一个斐波那契函数:

参数化测试示例FibonacciTest

对于data()方法返回的每个值(长度为2的数组),都会使用两个参数的构造器创建一个FibonacciTest实例。

11.2 字段注入

也可以使用@Parameter注解直接将数据值注入字段。例如:

参数化测试(字段注入)示例FieldInjectionFibonacciTest

目前这仅适用于公有字段(见#737)。

11.3 单个参数

从4.12-beta-3版本开始,如果测试只需要一个参数,就不必用数组包装,而是可以提供一个Iterable或对象数组:

1
2
3
4
@Parameters
public static Iterable<? extends Object> data() {
    return Arrays.asList("first test", "second test");
}

1
2
3
4
@Parameters
public static Object[] data() {
    return new Object[] {"first test", "second test"};
}

11.4 识别测试用例

为了便于识别参数化测试中的各个测试用例,可以使用@Parameters指定一个名称,这个名称可以包含占位符:

  • {index}:当前参数索引
  • {n}:第n个参数值(注意,单引号'应该转义为两个单引号''

参数化测试(指定名称)示例IdentifyTestCasesFibonacciTest

在上面的示例中,运行器将为测试用例创建类似于[3: fib(3)=2]的名字。如果不指定名称,则默认使用当前参数索引。

12.测试夹具

测试夹具(test fixture)是用作测试基准(baseline)的一组对象的固定状态,目的是确保有一个固定的环境来运行测试,使得结果可重复。例如:

  • 准备输入数据,创建/设置加对象或mock对象
  • 使用特定的数据集加载数据库
  • 拷贝特定的文件

JUnit提供了4个fixture注解:

  • 类级别的@BeforeClass@AfterClass:被标记的方法必须是static,仅在类中所有测试方法前后调用一次。
  • 方法级别的@Before@After:被标记的方法在每个测试前后调用一次。

另见Understanding JUnit method order execution

使用示例:

测试夹具示例TestFixturesExample

输出如下:

1
2
3
4
5
6
7
8
@BeforeClass setUpClass
@Before setUp
@Test test1()
@After tearDown
@Before setUp
@Test test2()
@After tearDown
@AfterClass tearDownClass

13.测试类别

可以使用@Category注解为测试指定一个或多个类别(category)。每个类别是一个类或接口,例如@Category(MyCategory.class)

Categories运行器仅运行指定类别的测试,有三种方式:

  • 使用@IncludeCategory注解标记测试套件,指定要包含的类别
  • 使用@ExcludeCategory注解标记测试套件,指定要排除的类别
  • 在命令行中使用--filter选项:
1
java org.junit.runner.JUnitCore --filter=org.junit.experimental.categories.IncludeCategories=MyCat1,MyCat2 --filter=org.junit.experimental.categories.ExcludeCategories=MyCat3,MyCat4

类别支持继承。例如,如果指定@IncludeCategory(SuperClass.class),则标记@Category({SubClass.class})的测试将会被运行。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// category marker
public interface FastTests {}
public interface SlowTests {}

public class TestA {
    @Test
    public void a() {
        fail();
    }

    @Category(SlowTests.class)
    @Test
    public void b() {}
}

@Category({SlowTests.class, FastTests.class})
public class TestB {
    @Test
    public void c() {}
}

@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@SuiteClasses({TestA.class, TestB.class})
public class SlowTestSuite {
    // Will run A.b and B.c, but not A.a
}

@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@ExcludeCategory(FastTests.class)
@SuiteClasses({TestA.class, TestB.class})
public class SlowTestSuite2 {
    // Will run A.b, but not A.a or B.c
}
This post is licensed under CC BY 4.0 by the author.