JUnit单元测试框架使用教程(适用于JUnit4及更低版本)

2020年02月17日 99 字 教程整理


JUnit是一个基于Java语言的单元测试框架。 本文以JUnit4作为基准版本进行JUnit的使用示意:

1. JUnit简介

JUnit是一个基于Java语言的单元测试框架,它是由Kent Beck和Erich Gamma编写的一个回归测试框架(regression testing framework),逐渐成为源于Kent Beck的sUnit的xUnit家族中最为成功的一个。
JUnit有它自己的JUnit扩展生态圈,多数Java的开发环境(IDE)都已经集成了JUnit作为单元测试的工具。

Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。

JUnit官网: https://junit.org/
当前最新版本的JUnit框架是JUnit5: https://junit.org/junit5/
而当前普遍广泛使用的版本是JUnit4: https://junit.org/junit4/

2. JUnit在项目中的引入及使用

2.1. JUnit框架的引入

在项目Build Path中引入JUnit工具包(jar包)或直接引用maven-depenency,如:

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
    <scope>test</scope>
</dependency>

2.2. 创建测试类

假设当前有一个待测试的类Calculator.class

public class Calculator {
 
    /**
     * 传入两个参数,求和
     * @param a
     * @param b
     * @return
     */
    public int add(int a,int b){
        return a+b;
    }
 
    /**
     * 传入两个参数,求差
     * @param a
     * @param b
     * @return
     */
    public int sub(int a,int b){
        return a-b;
    }
 
}

在这一步中,我们将为它创建一个名为CalculatorTest.class的测试类:

public class CalculatorTest {
    
}

2.3. 根据业务代码编写无参、无返回值的测试方法

public class CalculatorTest {
 
    @Test
    //测试 add()方法
    public void testAdd(){
        Calculator c = new Calculator();
        int result = c.add(1, 2);
        Assert.assertEquals(result, 3);
    }
 
    @Test
    //测试 sub()方法
    public void testSub(){
        Calculator c = new Calculator();
        int result = c.sub(2, 1);
        Assert.assertEquals(result, 1);
    }
 
}

【注】测试方法必须是public方法且无返回值(void),即公共、无返回数据。

测试方法可以抛出异常。

2.4. 运行JUnit Test

以Eclipse-IDE为例:

运行完成后,即可显示Test结果:

3. JUnit的常用注解释义

Junit中包含如下常用的注解:

注解 标记位置 注解说明 注解内部属性
@Test 方法 在JUnit4及以上版本,定义一个测试方法只需在方法前加上@Test即可。
【注】@Test测试方法必须是public、void方法,即公共、无返回数据。@Test方法允许抛出异常。
expected=XXException.class:如果程序按照当前测试方法进行,抛出XXException.class异常,则该测试方法通过。
timeout=xxx:如果当前测试方法在xxx毫秒之内完成,则测试通过,反之测试不通过。
@Ignore 方法 被忽略的测试方法:跳过被标记的测试方法,不运行 -
@Before 方法 在当前测试类中,每一个测试方法在运行之前均运行一次@Before标记的方法。
主要用于一些独立于用例之间的准备工作,如:准备一条测试数据等。
【注】@Before方法必须是public、void方法且不能为static。被标注方法不止运行一次,根据整个测试类的测试用例数而定。
-
@After 方法 在当前测试类中,每一个测试方法在运行完成后均运行一次@After标记的方法。
与@Before对应,主要用于部分独立用例完成之后的清理,如:移除一条测试数据等。
【注】@After方法必须是public、void方法且不能为static。被标注方法不止运行一次,根据整个测试类的测试用例数而定。
-
@BeforeClass 方法 在当前测试类中,在开始执行当前测试类的所有测试方法(@Test) 执行之前运行一次@BeforeClass标记的方法。
一般用于做一些测试前的准备工作,如:创建数据库连接、读取文件等。
【注】@BeforeClass方法必须是public、static、void方法,即公开、静态、无返回。这个被标记的方法只会运行一次。
-
@AfterClass 方法 在当前测试类中,当前测试类的所有测试方法(@Test) 执行完成之后运行一次@AfterClass标记的方法。
一般用于处理一些测试后续工作,例如清理数据,恢复现场。
【注】@AfterClass方法必须是public、static、void方法,即公开、静态、无返回。这个被标记的方法只会运行一次。
-
@RunWith(<运行器类>) 测试类 置于测试类名之前,用于确定这个测试类将如何的。不标注会使用默认运行器,如:@RunWith(JUnit4.class)。
测试运行器则决定了用什么方式或偏好去运行这些测试集/类/方法。
常见的运行器有:
@RunWith(Parameterized.class)参数化运行器:配合@Parameters使用JUnit的参数化功能;
@RunWith(Suite.class)或 @SuiteClasses({ATest.class,BTest.class,CTest.class}): 测试集运行器配合使用测试集功能;
@RunWith(JUnit4.class): JUnit4的默认运行器;
@RunWith(JUnit38ClassRunner.class): 用于兼容junit3.8的运行器;
其他运行器,如: @RunWith(SpringJUnit4ClassRunner.class)集成了Spring的测试功能。
@Parameters(JUnit4及更早版本) 方法 用于使用参数化测试功能。使用@Parameters注解的测试类必须指定@RunWith(Parameterized.class)运行器来进行修饰。
【注】@Parameters修饰的方法必须是public且为无参的static方法,并且必须返回Collection类型的结果。

示例代码:
        <br>@Parameters  
        <br>public static Collection prepareData()&#123;  
            <br>Object [][] object = &#123;&#123;-1,-2,-3&#125;,&#123;0,2,2&#125;,&#123;-1,1,0&#125;,&#123;1,2,3&#125;&#125;;  
            <br>return Arrays.asList(object);
        <br>&#125;
    </td>
    <td>@Parameters(name="&#123;index&#125;") <br>这种带有内部属性的注释方式使用较少,暂不详细介绍。</td>
</tr>
<tr>
    <td>@ParameterizedTest(JUnit5版本)</td>
    <td>方法</td>
    <td>用于使用参数化测试功能。 JUnit5对参数化测试支持进行了大幅度的改进和提升。在这里暂不具体展开,如需深入了解,详见JUnit5官方教程文档: <a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests">https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests</a> 。
    </td>
    <td>-</td>
</tr>
<tr>
    <td>@Rule</td>
    <td>成员变量/方法</td>
    <td>Rule是JUnit4.7加入的新特性,有点类似于拦截器,用于在测试方法执行前后添加额外的处理。<br>实际上@Rule是@Before,@After的另一种实现。<br>使用时需要放在实现了TestRule的成员变量上或者返回TestRule的方法上,且修饰符为public。<br>@Rule会作用于当前测试类每一个测试方法。<br><br>例如,使用JUnit框架中的ExpectedException类,需要声明ExpectedException异常:<br>@Rule<br>public ExpectedException thrown= ExpectedException.none();<br>或:<br>@Rule<br>public TestName name = new TestName();<br><br>@Test<br>public void testA() &#123;<br>assertEquals("testA", name.getMethodName());<br>&#125;<br></td>
    <td>-</td>
</tr>

JUnit5的增设了更多注解,详见官方文档: https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

4. JUnit的断言解析

断言(Assert)是编写测试用例的核心实现方式,即期望值与实际测试的比对,以此来判断测试是否通过。

更详细的断言说明,详见官方文档: https://github.com/junit-team/junit4/wiki/Assertionshttps://github.com/junit-team/junit4/wiki/Assertions-CN

断言方法 说明
assertArrayEquals(expecteds, actuals) 两个数组相等返回true
assertEquals(expected, actual) 两个对象等值(类似于equals)返回true
assertNotEquals(expected, actual) 与assertEquals对应,两个对象是不等值返回true
assertNull(object) 对象为空返回true
assertNotNull(object) 与assertNull对应,对象为非空返回true
assertSame(expected, actual) 两个对象的引用相等(类似于==)返回true
assertNotSame(unexpected, actual) 两个对象的引用不相等(类似于!=)返回true
assertTrue(condition) 查看运行结果是否为true
assertFalse(condition) 查看运行结果是否为false
assertThat(actual, matcher) 查看实际值是否满足指定的条件,满足则返回true。
assertThat一般会搭配allOf/anyOf/containsString/closeTo/hasItem/...等匹配符来使用。

例:
@Test
public void assertWithHamcrestMatcher() {
assertThat(calculator.subtract(4, 1), is(equalTo(3)));
}

@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")));
}

关于assertThat的匹配符的使用,可对照官方文档说明:
fail() 强制测试失败。

在JUnit4之前的版本中,fail()通常配合try/catch代码块来使用,对于没有抛出预期异常的代码,test case的运行结果应该为fail。例:
@Test
public void test_vertex_args() throws GrammarException, VertexNotExistException, MismatchException, WeightException, LabelException {
try {
GraphPoetFactory().creatGraph("test/file/GraphPoet_vertex_args.txt");
} catch (GrammarException ex) {
assertThat(ex.getMessage(), containsString("The vertex args is lack."));
}
fail("expected for GrammarException of The vertex args is lack.");
}
这与希望测试异常的结果一致:只有抛出预期异常,测试才算运行成功。
这种方法比较老,在JUnit4中,同样的目的可以用@Test(expected=Throwable.class)或ExpectedException的方式来替代。

JUnit5的断言提供了更多选择,详见其官方文档: https://junit.org/junit5/docs/current/user-guide/#writing-tests-assertions

5. JUnit测试类的几项编写原则

  • 测试方法必须使用@Test进行修饰。
  • 测试方法必须使用publicvoid进行修饰,且不能带有任何参数。
  • 测试代码位置应与业务代码的位置分离,如Maven项目中的: src/test/java
  • 测试类的包名称应该与和被测试类保持一致。
  • 测试单元中的每个方法必须可以独立运行,测试方法间不能有任何依赖。
  • 测试类建议使用<业务类名称>Test.class作为类名后缀。
  • 测试方法建议使用test作为方法前缀。

6. JUnit使用的一些总结

6.1. JUnit测试异常的方法汇总

假设我们有一段业务代码如下:

public class Student {
    public boolean canVote(int age) {
        if (i<=0) 
            throw new IllegalArgumentException("age should be +");
        if (i<18) 
            return false;
          else 
            return true;
    }
}

6.1.1. @Test(expected=Throwable.class)

这种方法代码更整洁清晰。不足的是:这个测试会有一点误差,因为异常会在方法的某个位置被抛出,但不一定在特定的某行;同时没有办法检查抛出异常的信息

测试代码示例:

@Test(expected = IllegalArgumentException.class)
public void canVoteExp() {
    new Student().canVote(0);
}

6.1.2. ExpectedException

这种方法除了可以设置异常的属性信息之外,还有一个优点:可以更加精确高效地定位到异常抛出的位置

测试代码示例:

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void canVoteExp() {
    Student student = new Student();
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("age should be +");
    student.canVote(0);
}

6.1.3. Try/catch with assert/fail

这种方法相对比较老,不建议过多使用。

测试代码示例:

@Test
public void canVoteExp() {
    Student student = new Student();
    try {
        student.canVote(0);
    } catch (IllegalArgumentException e) {
        assertThat(e.getMessage(), containsString("age should be +"));
    }
    fail("expected IllegalArgumentException for non + age");
}