学习Java单元测试框架之JUnit
单元测试
一、单元测试
如果你听说过“测试驱动开发”(TDD:Test-Driven Development),单元测试就不陌生。
单元测试(英语:Unit Testing)又称为模块测试
,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。
单元测试本质上也是通过编写代码,与普通代码的区别在于它是验证代码正确性的代码。也就是说单元测试是开发人员编写的、用于检测在特定条件下目标代码正确性的代码。所以单元测试属于白盒测试的一种。
黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值。
白盒测试:需要写代码的。关注程序具体的执行流程。()
最小可测单元
进行检查和验证,比如 C
语言中测试单元指一个函数,Java
1.2 单元测试有什么好处?
-
便于后期重构。单元测试可以为代码的重构提供保障,只要重构代码之后单元测试全部运行通过,那么在很大程度上表示这次重构没有引入新的BUG,当然这是建立在完整、有效的单元测试覆盖率的基础上。
-
优化设计。编写单元测试将使用户从调用者的角度观察、思考,特别是使用TDD驱动开发的开发方式,会让使用者把程序设计成易于调用和可测试,并且解除软件中的耦合。
-
文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
-
具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。
1.3 什么是单元测试用例?
单元测试用例就是一部分代码,可以确保另一端代码(方法)按预期工作。为了迅速达到预期的结果,就需要测试框架。比如 JUnit 是 Java 编程语言理想的单元测试框架。
每一项需求至少需要两个单元测试用例:一个正检验,一个负检验。如果一个需求有子需求,每一个子需求必须至少有正检验和负检验两个测试用例。
二、JUnit 概述
JUnit 是一个 Java
编程语言的单元测试框架。JUnit 在测试驱动的开发方面有很重要的发展,是起源于 JUnit 的一个统称为 xUnit 的单元测试框架之一。
JUnit 促进了“先测试后编码”的理念,强调建立测试数据的一段代码,可以先测试,然后再应用。这个方法就好比“测试一点,编码一点,测试一点,编码一点……”,增加了程序员的产量和程序的稳定性,可以减少程序员的压力和花费在排错上的时间。
2.2 JUnit的特点
-
JUnit 是一个开放的资源框架,用于编写和运行测试代码。
-
提供注解来识别测试方法。
-
提供断言来测试预期结果。
-
提供测试运行来运行测试。
-
JUnit 测试允许你编写代码更快,并能提高质量。
-
JUnit 优雅简洁。没那么复杂,花费时间较少。
-
JUnit 测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。
-
JUnit 测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。
-
JUnit 在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。
2.3 Junit 测试框架
JUnit 是一个回归测试框架,被开发者用于实施对应用程序的单元测试,加快程序编制速度,同时提高编码的质量。JUnit 测试框架具有以下重要特性:
-
测试工具
一整套固定的工具用于基线测试。测试工具的目的是为了确保测试能够在共享且固定的环境中运行,因此保证测试结果的可重复性。它包括:
-
在所有测试调用指令发起前的
setUp()
方法。 -
在测试方法运行后的
tearDown()
方法。
-
-
测试套件意味捆绑几个测试案例并且同时运行。在 JUnit 中,
@RunWith
和@Suite
都被用作运行测试套件。 -
测试运行器
用于执行测试案例。
-
测试分类
测试分类是在编写和测试 JUnit 的重要分类。几种重要的分类如下:
-
包含一套断言方法的测试断言
-
包含规定运行多重测试工具的测试用例
-
包含收集执行测试用例结果的方法的测试结果
-
在准备使用JUnit测试框架时,我们首先需要在项目中添加所需要的依赖Jar包,Jar包有两个分别是:junit-4.13-rc-2
和hamcrest-core-1.3
。如果是使用的maven构建的项目,直接在pom文件中引入如下坐标:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
3.1 注意事项
-
测试方法必须使用 @Test 修饰
-
测试方法必须使用 public void 进行修饰,不能带参数
-
一般使用单元测试会新建一个 test 目录存放测试代码,在生产部署的时候只需要将 test 目录下代码删除即可
-
测试代码的包应该和被测试代码包结构保持一致
-
测试单元中的每个方法必须可以独立测试,方法间不能有任何依赖
-
测试类一般使用 Test 作为类名的后缀
-
测试方法使一般用 test 作为方法名的前缀
-
Failure
:一般是由于测试结果和预期结果不一致引发的,表示测试的这个点发现了问题 -
error
:是由代码异常引起的,它可以产生于测试代码本身的错误,也可以是被测试代码中隐藏的 bug
3.3 常用注解
注解就好像可以在你的代码中添加并且在方法或者类中应用的元标签。
JUnit 中的这些注解为我们提供了测试方法的相关信息,哪些方法将会在测试方法前后应用,哪些方法将会在所有方法前后应用,哪些方法将会在执行中被忽略。 JUnit 中的注解的列表以及他们的含义:
-
@Test
:将一个普通方法修饰成一个测试方法 ,这个注解说明依附在 JUnit 的 public void 方法可以作为一个测试案例。-
@Test(excepted=xx.class): xx.class
表示异常类,表示测试的方法抛出此异常时,认为是正常的测试通过的 。 -
@Test(timeout = 毫秒数)
:测试方法执行时间是否符合预期。
-
-
@BeforeClass
: 会在所有的方法执行前被执行,static 方法 (全局只会执行一次,而且是第一个运行),在 public void 方法加该注解是因为该方法需要在类中所有方法前运行。 -
@AfterClass
:会在所有的方法执行之后进行执行,static 方法 (全局只会执行一次,而且是最后一个运行),它将会使方法在所有测试结束后执行。这个可以用来进行清理活动。 -
@Before
:会在每一个测试方法被运行前执行一次,有些测试在运行前需要创造几个相似的对象。在 public void 方法加该注解是因为该方法需要在 test 方法前运行。 -
@After
:会在每一个测试方法运行后被执行一次, 如果你将外部资源在 Before 方法中分配,那么你需要在测试运行后释放他们。在 public void 方法加该注解是因为该方法需要在 test 方法后运行。 -
:所修饰的测试方法会被测试运行器忽略
-
@RunWith
:可以更改测试运行器 org.junit.runner.Runner -
@Parameters
:参数化注解
3.4.1 创建Calculator类
package cn.imyjs.junit;
/**
* @author YJS
* @classname Calculator
* @description 计算器类,提供整数的加法、减法运算方法。
* @date 2023/2/16 11:28
* @webSite www.imyjs.cn
*/
public class Calculator {
public int add(int a, int b){
return a + b;
}
public int sub(int a, int b){
return a - b;
}
}
3.4.2 创建测试用例类
package cn.imyjs.junit.test;
import cn.imyjs.junit.Calculator;
import org.junit.*;
/**
* @author YJS
* @classname CalculatorTest
* @description 测试 Calculator类的加减方法
* @date 2023/2/16 11:31
* @webSite www.imyjs.cn
*/
public class CalculatorTest {
@Before
public void before(){
System.out.println("before....");
}
@After
public void after(){
System.out.println("after...");
}
@BeforeClass
public static void beforeClass(){
System.out.println("BeforeClass....");
}
@Test
public void testAdd(){
Calculator calculator = new Calculator();
int sum = calculator.add(5, 6);
System.out.println("测试两数相加方法");
Assert.assertEquals(11, sum);
}
@Test
public void testSub(){
Calculator calculator = new Calculator();
int sum = calculator.sub(5, 6);
System.out.println("测试两数相减方法");
Assert.assertEquals(-1, sum);
}
@AfterClass
public static void afterClass(){
System.out.println("AfterClass...");
}
}
public class Main {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(CalculatorTest.class);
for (Failure f : result.getFailures()) {
System.out.println(f.toString());
}
System.out.println(result.wasSuccessful());
}
}
3.4.4 分析结果
程序执行后在控制台打印结果如下:
BeforeClass....
before....
测试两数相加方法
after...
before....
测试两数相减方法
after...
AfterClass...
true
Process finished with exit code 0
-
beforeClass()
方法首先执行,并且只执行一次。注意:必须为static修饰 -
afterClass()
方法最后执行,并且只执行一次。注意:必须为static修饰 -
before()
方法针对每一个测试用例执行,但是是在执行测试用例之前。 -
after()
方法针对每一个测试用例执行,但是是在执行测试用例之后 -
在
before()
方法和after()
3.5 测试套件
在实际项目中,随着项目进度的开展,单元测试类会越来越多,可是直到现在我们还只会一个一个的单独运行测试类,这在实际项目实践中肯定是不可行的。为了解决这个问题,JUnit 提供了一种批量运行测试类的方法,叫做测试套件。
-
创建一个空类作为测试套件的入口。
-
使用注解
org.junit.runner.RunWith
和org.junit.runners.Suite.SuiteClasses
修饰这个空类。 -
将
org.junit.runners.Suite
作为参数传入注解RunWith
,以提示 JUnit 为此类使用套件运行器执行。 -
将需要放入此测试套件的测试类组成数组作为注解
SuiteClasses
的参数。 -
保证这个空类使用 public 修饰,而且存在公开的不带有任何参数的构造函数
创建两个功能类:
public class Demo1 {
public String method(){
System.out.println(" Demo1 method run...");
return "OK";
}
}
public class Demo2 {
public String method(){
System.out.println(" Demo2 method run...");
return "OK";
}
}
分别为两个功能类创建两个测试实例类:
public class Demo1Test {
@Test
public void testMethod(){
Demo1 demo1 = new Demo1();
Assert.assertEquals("OK", demo1.method());
}
}
public class Demo2Test {
@Test
public void testMethod(){
Demo2 demo2 = new Demo2();
Assert.assertEquals("OK", demo2.method());
}
}
测试套件集中测试:
@RunWith(Suite.class)
@Suite.SuiteClasses({
Demo1Test.class, Demo2Test.class
})
public class AllTest {
}
有时可能会发生我们的代码还没有准备好的情况,这时测试用例去测试这个方法或代码的时候会造成失败。@Ignore 注释会在这种情况时帮助我们。
-
一个含有 @Ignore 注释的测试方法将不会被执行。
-
如果一个测试类有 @Ignore 注释,则它的测试方法将不会执行。
package cn.imyjs.junit;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
/**
* @author YJS
* @classname IngnoreTest
* @description
* @date 2023/2/16 18:38
* @webSite www.imyjs.cn
*/
public class IgnoreTest {
@Test
@Ignore
public void testAdd(){
Calculator calculator = new Calculator(5, 10);
System.out.println("test add method");
Assert.assertEquals(15,calculator.add());
}
@Test
public void testSub(){
Calculator calculator = new Calculator(5, 10);
System.out.println("test sub method");
Assert.assertEquals(-5, calculator.sub());
}
}
// Test ignored.
// test sub method
// Process finished with exit code 0
3.7 时间测试
Junit 提供了一个暂停的方便选项。如果一个测试用例比起指定的毫秒数花费了更多的时间,那么 Junit 将自动将它标记为失败。timeout 参数和 @Test 注释一起使用。现在让我们看看活动中的 @test(timeout)。
public class Demo3 {
public String method(){
try {
Thread.sleep(5);
System.out.println(" Demo3 method run...");
return "OK";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
测试用例类:
public class TimeoutTest {
@Test(timeout = 6000)
public void testMethod(){
Demo3 demo3 = new Demo3();
Assert.assertEquals("OK", demo3.method());
}
}
3.8 异常测试
Junit 用代码处理提供了一个追踪异常的选项。你可以测试代码是否它抛出了想要得到的异常。expected 参数和 @Test 注释一起使用。被测试代码抛出指定异常不报错仍为测试成功。
public class Demo4 {
public String method(){
System.out.println(" Demo4 method run...");
int i = 5 / 0;
return "OK";
}
}
public class ExceptionTest {
@Test(expected = ArithmeticException.class)
public void testMethod(){
Demo4 demo4 = new Demo4();
Assert.assertEquals("OK", demo4.method());
}
}
3.9 参数化测试
Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同一个测试。你将遵循 5 个步骤来创建参数化测试。
-
用
@RunWith(Parameterized.class)
来注释 test 类。 -
创建一个由
@Parameters
注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合。 -
创建一个公共的构造函数,它接受和一行测试数据相等同的东西。
-
为每一列测试数据创建一个实例变量。
-
用实例变量作为测试数据的来源来创建你的测试用例。
创建功能类:
public class PrimeNumberChecker {
public Boolean validate(final Integer primeNumber) {
// 在final修饰的方法参数中,如果修饰的是基本类型,那么在这个方法的内部,基本类型的值是不能够改变的,
// 但是如果修饰的是引用类型的变量,那么引用类型变量所指的引用是不能够改变的
// 但是引用类型变量的值是可以改变的。
for (int i = 2; i < (primeNumber / 2); i++) {
if (primeNumber % i == 0) {
return false;
}
}
return true;
}
}
测试类:
@RunWith(Parameterized.class) // 用 @RunWith(Parameterized.class)来注释 test 类。
public class PrimeNumberCheckerTest {
private Integer inputNumber;
private Boolean expectedResult;
private PrimeNumberChecker primeNumberChecker;
@Before
public void initialize() {
primeNumberChecker = new PrimeNumberChecker();
}
// 创建一个公共的构造函数,它接受和一行测试数据相等同的东西。
// 每次runner触发时,它都会传递从我们在primeNumbers()方法中定义的参数
public PrimeNumberCheckerTest(Integer inputNumber,
Boolean expectedResult) {
this.inputNumber = inputNumber;
this.expectedResult = expectedResult;
}
// 创建一个由 @Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合。
@Parameterized.Parameters
public static Collection primeNumbers() {
return Arrays.asList(new Object[][] {
{ 2, true },
{ 6, false },
{ 19, true },
{ 22, false },
{ 23, true }
});
}
// 该测试将运行4次,因为我们定义了5个参数
@Test
public void testPrimeNumberChecker() {
System.out.println("Parameterized Number is : " + inputNumber);
Assert.assertEquals(expectedResult,
primeNumberChecker.validate(inputNumber));
}
}
//Parameterized Number is : 2
//Parameterized Number is : 6
//Parameterized Number is : 19
//Parameterized Number is : 22
//Parameterized Number is : 23
//
//Process finished with exit code 0
四、JUnit 中的重要的 API
JUnit 中的最重要的程序包是 junit.framework
它包含了所有的核心类。一些重要的类列示如下:
序号 | 类的名称 | 类的功能 |
---|---|---|
1 | Assert | assert 方法的集合 |
2 | TestCase | 一个定义了运行多重测试的固定装置 |
3 | TestResult | TestResult 集合了执行测试样例的所有结果 |
4 | TestSuite | TestSuite 是测试的集合 |
4.1 Assert 类
断言可以简单理解为判断。Junit所有的断言都包含在 Assert 类中。
这个类提供了一系列的判断测试结果是否达到预期的声明方法。只有失败的声明方法才会被记录,也就是被打印到控制台。Assert类的重要方法列式如下:
序号 | 方法和描述 |
---|---|
1 | void assertEquals(boolean expected, boolean actual) 检查两个变量或者等式是否平衡 |
2 | void assertFalse(boolean condition) 检查条件是假的 |
3 | void assertNotNull(Object object) 检查对象不是空的 |
4 | void assertNull(Object object) 检查对象是空的 |
5 | void assertTrue(boolean condition) 检查条件为真 |
6 | void fail() 在没有报告的情况下使测试不通过 |
package cn.imyjs.junit.test;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
/**
* @author YJS
* @classname AssertTest
* @description
* @date 2023/2/16 16:44
* @webSite www.imyjs.cn
*/
public class AssertTest {
@Test
public void testAssert(){
int num = 10;
String str = "YJS";
Object obj = null;
assertEquals("YJS", str);
assertFalse(num > 20);
assertNull(obj);
}
public static void main(String[] args) {
Result result = JUnitCore.runClasses(AssertTest.class);
for (Failure f: result.getFailures()) {
System.out.println(f.getMessage());
}
System.out.println(result.wasSuccessful());
}
}
// import static org.junit.Assert.*; 方式导入,可以直接使用Assert的全部方法
// 正确执行,控制台打印信息 true
public abstract class TestCase extends Assert implements Test
测试样例定义了运行多重测试的固定格式。TestCase
类的一些重要方法列式如下:
序号 | 方法和描述 |
---|---|
1 | int countTestCases() 为被run(TestResult result) 执行的测试案例计数 |
2 | TestResult createResult() 创建一个默认的 TestResult 对象 |
3 | String getName() 获取 TestCase 的名称 |
4 | TestResult run() 一个运行这个测试的方便的方法,收集由TestResult 对象产生的结果 |
5 | void run(TestResult result) 在 TestResult 中运行测试案例并收集结果 |
6 | void setName(String name) 设置 TestCase 的名称 |
7 | void setUp() 创建固定装置,例如,打开一个网络连接 |
8 | void tearDown() 拆除固定装置,例如,关闭一个网络连接 |
9 | String toString() 返回测试案例的一个字符串表示 |
package cn.imyjs.junit.test;
import junit.framework.TestCase;
/**
* @author YJS
* @classname TestCaseTest
* @description
* @date 2023/2/16 16:55
* @webSite www.imyjs.cn
*/
public class TestCaseTest extends TestCase {
int num1 = 5;
int num2 = 20;
/**
* 创建固定装置,例如,打开一个网络连接
* 作用类似于在方法上添加@Before 注解
* 方法针对每一个测试用例执行,但是是在执行测试用例之前。
*/
protected void setUp() {
System.out.println("setUp...");
}
/**
* 无需添加@Test注解
*/
public void testAdd() {
System.out.println("No of Test Case = "+ this.countTestCases());
String name= this.getName();
System.out.println("Test Case Name = "+ name);
this.setName("testNewAdd");
String newName= this.getName();
System.out.println("Updated Test Case Name = "+ newName);
int result = num1 + num2;
assertEquals(25, result);
}
/**
* 无需添加@Test注解
*/
public void testSub() {
int result = num1 - num2;
assertEquals(-15, result);
}
/**
* 无需添加@Test注解
*/
public void testMul() {
int result = num1 * num2;
assertEquals(100, result);
}
/**
* tearDown 用于关闭连接或清理活动
* 作用类似于在方法上添加@After 注解
* 方法针对每一个测试用例执行,但是是在执行测试用例之后。
*/
public void tearDown() {
System.out.println("tearDown....");
}
}
4.3 TestResult 类
TestResult
类收集所有执行测试案例的结果。它是收集参数层面的一个实例。这个实验框架区分失败和错误。失败是可以预料的并且可以通过假设来检查。错误是不可预料的问题就像 ArrayIndexOutOfBoundsException
。TestResult
序号 | 方法和描述 |
---|---|
1 | void addError(Test test, Throwable t) 在错误列表中加入一个错误 |
2 | void addFailure(Test test, AssertionFailedError t) 在失败列表中加入一个失败 |
3 | void endTest(Test test) 显示测试被编译的这个结果 |
4 | int errorCount() 获取被检测出错误的数量 |
5 | Enumeration errors() 返回错误的详细信息 |
6 | int failureCount() 获取被检测出的失败的数量 |
7 | void run(TestCase test) 运行 TestCase |
8 | int runCount() 获得运行测试的数量 |
9 | void startTest(Test test) 声明一个测试即将开始 |
10 | void stop() 标明测试必须停止 |
4.4 TestSuite 类
TestSuite
类是测试的组成部分。它运行了很多的测试案例。TestSuite
类的一些重要方法列式如下:
序号 | 方法和描述 |
---|---|
1 | void addTest(Test test) 在套中加入测试。 |
2 | void addTestSuite(Class<? extends TestCase> testClass) 将已经给定的类中的测试加到套中。 |
3 | int countTestCases() 对这个测试即将运行的测试案例进行计数。 |
4 | String getName() 返回套的名称。 |
5 | void run(TestResult result) 在 TestResult 中运行测试并收集结果。 |
6 | void setName(String name) 设置套的名称。 |
7 | Test testAt(int index) 在给定的目录中返回测试。 |
8 | int testCount() 返回套中测试的数量。 |
9 | static Test warning(String message) 返回会失败的测试并且记录警告信息。 |
package cn.imyjs.junit.test;
import junit.framework.TestResult;
import junit.framework.TestSuite;
/**
* @author YJS
* @classname JunitTestSuite
* @description
* @date 2023/2/16 17:33
* @webSite www.imyjs.cn
*/
public class JunitTestSuite {
public static void main(String[] args) {
TestSuite testSuite = new TestSuite(CalculatorTest.class, AssertTest.class);
TestResult result = new TestResult();
testSuite.run(result);
// 获得运行测试的数量
System.out.println("Number of test cases = " + result.runCount());
// Number of test cases = 2
}
}
编程那点事儿

编程那点事儿