测试方法之JUnit单元测试
程序员文章站
2024-03-15 18:33:12
...
1. JUint 简介
- JUnit 是一个开放源代码的 Java 测试框架,用于编写和运行可重复的测试。
- JUnit 测试是程序员测试,即所谓白盒测试,是一个 Java 语言的单元测试框架,多 数 Java 的开发环境都已经集成了 JUnit 作为单元测试的工具。
- JUnit 在极限编程和重构(refactor)中被极力推荐使用,因为在实现自动单元测试 的情况下可以大大的提高开发的效率。
- 每编写完一个函数之后,都应该对这个函数的方方面面进行测试,这样的测试我们 称之为单元测试。
- 在编写大型程序的时候,需要写成千上万个方法或函数,也许我们在程序中只 用到该函数的一小部分功能,并且经过调试可以确定,这一小部分功能是正确 的。但是,我们同时应该确保每一个函数都完全正确,因为如果我们今后如果 对程序进行扩展,用到了某个函数的其他功能,而这个功能有 bug 的话,那绝 对是一件非常郁闷的事情。
2. JUnit 中的注解
- JUnit 使用注解进行单元测试。
- 注解用于修饰测试方法或测试类,写在测试方法或测试类的前面。同时,使用 这些注解时,需要导入相应的包。
- 比较重要的注解大致有 Fixture 注解、@Test 注解、@Ignore 注解、@Parameters 注 解、@RunWith 注解。
2.1 Fixture 注解
- 表示“在某些阶段必然被调用的代码”。
- 一般包括@Before 注解、@After 注解、@BeforeClass 注解、@AfterClass 注解,这 些注解一般用于修饰测试方法,测试方法名随意。
- @Before 注解
- @Before 修饰的方法在每个测试方法之前执行。
- @After 注解
- @After 修饰的方法在在每个测试方法之后执行。
- @BeforeClass 注解
- @BeforeClass 修饰的方法在所有测试方法执行之前执行。
- @AfterClass 注解
- @AfterClass 修饰的方法在所有测试方法执行之后执行。
2.2 @Test 注解
- @Test 注解
- 用于修饰测试方法,表示要对被测试类的某个或某些方法进行测试。
- @Test(timeout=xxx)注解
- xxx 表示时间,以 ms 为单位;
- 一般称为限时测试或超时测试,用于设置当前测试方法在一定时间内运行完, 否则返回错误。
- 对于那些逻辑很复杂,循环嵌套比较深的程序,很有可能出现死循环,因 此一定要采取一些预防措施,限时测试是一个很好的解决方案,给测试函 数或方法设定一个执行时间,超过了这个时间,程序就会被系统强行终止, 并且系统还会汇报该函数结束的原因是因为超时,这样就可以发现这些 Bug 了。
2.3 @Ignore 注解
- @Ignore 注解用于修饰测试方法,表示忽略测试用例;
- 其含义是“某些方法尚未完成,暂不参与此次测试”。这样的话测试结果就会提示 你有几个测试被忽略,而不是失败。一旦你完成了相应函数,只需要把@Ignore 标 注删去,就可以进行正常的测试。
2.4 @Parameters 注解
- 用于修饰产生数据集合的方法,用于参数化。
- 测试时,可能需要测试大量不同的路径,从而要使用大量的数据,使用@Parameters 注解只需要编写一个测试代码即可众多数据进行多次测试。
2.5 @RunWith 注解
- 用于指定一个测试运行器(Runner),@RunWith 用来修饰类,而不能修饰函数。
- 只要对一个类指定了 Runner,那么这个类中的所有函数都被这个 Runner 来调用。
- 常用的内置测试运行器有 Parameterized(参数化数据)和 Suite(测试集)。
3. JUnit 中的方法
3.1 断言
- 用来断定程序中是否存在缺陷。
- assertEquals(预期结果,实际结果)
- 用于测试期望结果的断言,即测试两个对象是否相等,这个方法使用非常多。
- fail(错误消息内容)
- 其中错误消息可选,假如提供,将会在发生错误时报告这个消息。该断言会使 测试立即失败,通常用在测试不能达到的分支上(如异常)。
3.2 setUp 和 tearDown
- 在实际的测试中我们测试某个类的功能是常常需要执行一些共同的操作,完成以后 需要销毁所占用的资源(例如网络连接、数据库连接,关闭打开的文件等),JUnit 提供了 setUp 方法、tearDown 方法、setUpBeforeClass 方法、tearDownAfterClass 方法。
- setUp 方法
- 在每个测试方法之前都会运行。
- 主要实现测试前的初始化工作。
- tearDown 方法
- 在每个测试方法结束以后都会执行。
- 主要实现测试完成后的垃圾回收等工作。
- setUpBeforeClass 方法
- 在所有测试方法执行之前执行。
- tearDownAfterClass 方法
- 在所有测试方法执行之后执行。
- 实际使用时,setUp 方法可用@Before 注解代替,tearDown 方法可用@After 注解代 替,setUpBeforeClass 方法可用@BeforeClass 代替,tearDownAfterClass 方法可用 @AfterClass 代替。
4. JUnit 的安装和使用流程
4.1 第 1 步,新建 Java 项目
- 为项目命名,新建一个包并命名,新建一个类命名如 Calculator(也可以暂时不建 类,这个类是被测类),如下图。
4.2 第 2 步,将 JUnit 引入项目
- 右击项目 Java—>选择“构建路径”—>点击“配置构建路径”,如下图。
在弹出的窗口中点击“库”选项卡,点击“添加库”按钮,如下图。
在弹出的窗口中选择“JUnit”,如下图。
之后选择“JUnit”版本,如下图。
最后的效果如下图。
4.3 第 3 步,编写被测类的代码
- 这是一个能够简单实现加减乘除、平方、开方的计算器类,然后对这些功能进 行单元测试。编写完成后,类无需编译。
public class Calculator {
private static int result; // 静态变量,用于存储运行结果
public void add(int n) {
result = result + n;
}
public void substract(int n) {
result = result - 1; //Bug: 正确的应该是 result =result-n
}
public void multiply(int n) {
} //此方法尚未写好
public void divide(int n) {
result = result / n;
}
public void square(int n) {
result = n * n;
}
public void squareRoot(int n) {
for (; ;) ; //Bug: 死循环
}
public void clear() { //将结果清零
result = 0;
}
public int getResult() {
return result;
}
}
4.4 第 4 步,创建 JUnit 测试用例类
- 在 Eclipse 的“包资源管理器”中找到 Calculator 类,右击,在弹出菜单中选择“新 建”—>“JUnit 测试用例”,如下图。
- 在打开的窗口中选择“新建 JUnit 测试”,这里使用注解方式编写测试方法, 而没有使用 setUp 等方法,所以这些方法均没有选中,如下图。
- 需要注意
- 测试类(如 CalculatorTest)是一个独立的类,没有任何父类。
- 测试类的名字可以任意命名,没有任何局限性,所以我们不能通过类的声 明来判断它是不是一个测试类,它与普通类的区别在于它内部的方法的声 明。
- 在下图中选择需要测试的方法,在前面打勾。
之后系统会自动生成一个新类 CalculatorTest,里面包含一些空的测试用例, 之后需要对这些测试用例进行修改才可使用。如下图所示。
4.5 第 5 步,运行测试代码
- 在步骤 4 的基础上编写测试代码(这里暂时不编写,后文给出代码),右击 CalculatorTest 类,选择“运行方式”—>“JUnit 测试”,如下图。
4.6 第 6 步,分析测试结果
如下图。结果的具体含义后文将通过具体案例来说明。
- 红色长条是测试时出现了错误或者故障,如图中的测试结果是
- 红条上面的“运行次数:5/5 错误:0 故障次数:4”
- 表示共进行了 5 个测试,其中 0 个测试代码出现错误,4 个测试失败(这 4 个失败的测试,未必是真正发现了缺陷,因为有的测试代码还没有编 写)。
5. JUnit 单元测试案例
5.1 在测试类中创建一个被测类对象
private static Calculator calculator=new Calculator();
5.2 使用@Test 进行测试
这里以测试 Calculator 类的 add 方法为例进行说明。
- 被测试的 add 方法的代码如下:
public void add(int n) {
result = result + n;
}
- 获取结果的方法 getResult 的代码如下:
public int getResult() {
return result;
}
- 测试 add 方法的类
@Test
public void testAdd() {
fail("尚未实现");
}
- 其中@Test 表示其后的方法 testAdd 是测试方法。
- 对于测试方法的声明有如下要求 名字可以随便取,没有任何限制,但是返回值必须为 void,而且不能有任 何参数。
- 编写测试代码
@Test
public void testAdd() {
calculator.add(2);
assertEquals(2,calculator.getResult());
calculator.add(3);
assertEquals(5,calculator.getResult());
}
- 说明:
- calculator.add(2)表示调用 add 方法,参数为 2,由于 Calculator 类中使用 private static int result 定义了变量 result,其初值为 0,所以执行完 calculator.add(2)后,预期结果应该是 2。
- assertEquals 是 Assert 类中的一个静态方法,一般的使用方式是 Assert. assertEquals(),使用此语句时需要包含包 org.junit.Assert.,实际包含包的语句是 import static org.junit.Assert.,其中使用了静态包含(使用 static 关键字)后,前面的类名就可以省略了,使用起来更加方便。
- 这 里 的 测 试 代 码 中 使 用 assertEquals(2,calculator.getResult()) 表 示 执 行 calculator.getResult()方法获得结果与 2 进行比较。
- 运行测试方法,结果如下图。
- 测试方法列表(prog.CalculatorTset 下面的多行)
- 绿色对勾:表示测试通过
- 蓝色差号:表示测试代码有错误或者测试失败
- 选中某个测试方法后,故障跟踪部分会显示测试代码的错误或者发现的缺 陷,如果没有缺陷则什么都不显示。
5.3 使用@Before 进行测试前准备
5.3.1 测试 divide 方法
- divide 方法的代码如下:
public void divide(int n) {
result = result / n;
}
- 编写测试方法,代码如下:
@Test
public void testDivide() {
calculator.add(8);
calculator.divide(2);
assertEquals(4, calculator.getResult());
}
- 说明:
- calculator.divide(2)表示将 result 的值除以 2 后放入 result 中,最后 result 应该等
于 4; - 使用 assertEquals(4, calculator.getResult())表示判断结果 result 是否等于 4。
- calculator.divide(2)表示将 result 的值除以 2 后放入 result 中,最后 result 应该等
- 运行测试代码,结果如下图。
- 点击 testDivide 方法,在故障跟踪下可以看到“expected:<4> but was:<6>”
- 表示预期结果是 4,实际结果是 6,测试没有通过。
- 我们看代码中的语句 result = result / n,应该是没有问题的,但是为什么这 里测试代码却发现了缺陷?事实上,如果我们应该回忆一下在执行完 testAdd 方法时 result 已经等于 5 了!调用 testDivide 方法时,执行 calculator.add(8)后 result 等于 13,执行 calculator.divide(2)后 result 等于 6, 正好与故障跟踪中的信息一致!
- 这说明,测试方法 testAdd 和 testDivide 发生了相互干扰,如果期望多个 测试方法间相互独立,即执行每个测试方法前都希望 result 等于 0,可以 使用@Before 注解。
5.3.2 加入@Before 注解
- 在 CalculatorTest 类中添加一个方法,用于在执行每个测试方法前先将 result 变量的 值清零。代码如下:
@Before
public void initiate(){
calculator.clear();
}
- calculator.clear 方法将用于将结果 result 清零,代码如下:
public void clear() {
result = 0;
}
-
@Before 在此处不能省略,表示执行每隔测试方法前先执行@Before 修饰的方 法。
-
@Before 后的方法名随意,只要符号命名规则即可。
-
重新执行测试,结果显示测试通过了。运行结果如下。
5.3.3 测试 substract 方法
- substract 方法的代码如下:
public void substract(int n) {
result = result - 1; //Bug: 正确的应该是 result=result-n
}
- 编写测试方法,代码如下:
@Test
public void testSubstract() {
calculator.add(10);
calculator.substract(2);
assertEquals(8, calculator.getResult());
}
- 运行测试后,结果如下图。
- 结果显示 substract 方法存在缺陷,预期结果是 8,实际结果是 9。
5.4 使用@Ignore 忽略测试
- multiply 方法并没有编写方法体,可以使用@Ignore 忽略该测试。测试代码如下。
@Ignore("代码未实现,暂时不测")
@Test
public void testMultiply() {
}
- 测试运行结果如下图,由于测试被忽略了,其结果显示的是测试通过。
- testMultiply 方法不必编写具体的代码,待将来 multiply 方法实现以后,就可以 编写 testMultiply 方法了。
- @Ignore(“代码未实现,暂时不测”)表示后面的测试方法暂不执行
- 注意:其后的@Test 注解不能省略。
- 运行次数:5/5(1 skipped)表示 1 个测试被忽略。
5.5 使用@Test(timeout=?)进行超时限制测试
- 在前面代码的基础上,继续测试 squareRoot 方法,squareRoot 方法的代码如下:
public void squareRoot(int n) {
for (; ;) ; //Bug: 死循环
}
- 当方法中使用循环时,使用@Test(timeout=?)可以测试方法中的循环是否陷入了死 循环。测试代码如下:
@Test(timeout=2000)
public void testSquareRoot() {
calculator.squareRoot(4);
assertEquals(2, calculator.getResult());
}
- 测试运行结果如下图。
- timeout 的单位是毫秒,根据具体情况设置此时间即可。
5.6 使用@RunWith 和@Parameters 进行参数化测试
5.6.1 什么是参数化
- 怎么测试多分支?
- 如一个对考试分数进行评价的函数
- 返回值分别为“优秀,良好,一般,及格,不及格”
- 在编写测试的时候,如果编写 5 个测试方法,进而测试 5 种情况,是 一件很麻烦的事情。
- 如一个对考试分数进行评价的函数
- 为了简化类似的测试,JUnit 提出了“参数化测试”的概念,只写一个测试函 数,把这若干种情况作为参数传递进去,一次性的完成测试。
5.6.2 创建用@RunWith(Parameterized.class)注解的测试类
- 继续使用先前的例子,测试函数 square,其代码如下:
public void square(int n) {
result = n * n;
}
这里假设暂且测试三类数据:正数、0、负数。
- 若使用参数化测试,必须指定一个 Runner,这个 Runner 就是 Parameterized, 其写法是@RunWith(Parameterized.class)。
- 必须生成一个新的测试类,不能与其他测试共用同一个类;
- 必 须 在 类 前 使 用 @RunWith(Parameterized.class) 指 定 运 行 器 为 Parameterized;
- 在类中创建一个 Calculator 的对象以便调用其被测试的方法。 这里假设新建测试类命名为 SquareTest,代码如下:
@RunWith(Parameterized.class) //注意包含相应的包
public class SquareTest {
private static Calculator calculator= new Calculator();
}
5.6.3 在新测试类中添加存储参数的成员变量
- 在类中定义两个变量,一个用于存放参数,一个用于存放期待的结果。
private int param;
private int result;
5.6.4 使用@Parameters 修饰测试数据集方法
- 下面编写获得测试数据集的方法,该方法必须使用@Parameters 标注进行修饰;
- 方法可以任意命名;
- 集合中数据的顺序无所谓,但编写该类的测试人员必须记住它们的意义。 下面的代码中第 1 个数字表示参数,第 2 个表示预期结果。
@Parameters
public static Collection caseData(){
return Arrays.asList(new Object[][] { {2, 4}, {0, 0}, {-3, 9} });
}
5.6.5 编写类的构造函数
- 功能是对先前定义的两个参数进行初始化。
- 注意参数的顺序要和上面的数据集合的顺序保持一致。
- 如果前面的顺序是{参数,期待的结果},那么构造函数的顺序也要是 “构造函数(参数, 预期结果)”。
- 代码如下:
- 注意参数的顺序要和上面的数据集合的顺序保持一致。
//构造函数,对变量进行初始化
public SquareTest(int param, int result){
this.param=param;
this.result=result;
}
5.6.6 编写测试方法
- 代码如下
@Test
public void squareTest(){
calculator.square(param);
assertEquals(result, calculator.getResult());
}
- 运行结果如下
- 运行结果显示,3
5.6.7 读取文件中的数据进行参数化
- 打开文件
File file=new File(文件路径) - 读取文件
FileReader bytes=new FileReader(file) BufferedReader chars=new BufferedReader(bytes); - 读取文件中的行
while((row=chars.readLine())!=null) - 拆分行中的列
row.split("\t") - 关闭文件
chars.close()
5.7 使用@RunWith 和@Suite.SuiteClasses 运行测试集
5.7.1 什么是测试集
- 也称打包测试、测试套件。
- 在一个项目中,只写一个测试类是不可能的,我们会写出很多很多个测试类。 可是这些测试类必须一个一个的执行,比较麻烦。
- JUnit 提供运行测试集的功能,将所有需要运行的测试类集中起来,一次性的 运行完毕,大大的方便测试工作。
5.7.2 @RunWith(Suite.class)和@Suite.SuiteClasses 注解
- 打包测试也需要使用一个 Runner,需要向@RunWith 标注传递一个参数 Suite.class。
- 同时,还需要另外一个标注@Suite.SuiteClasses,来表明这个类是一个打包测试类,在内部需要提供需要运行的测试类的名字。
- 新的测试类的类名随便起一个,内容全部为空既可。 新建一个 Junit 测试用例 AllTest,代码如下:
@RunWith(Suite.class)
@Suite.SuiteClasses({ CalculatorTest.class, SquareTest.class })
public class AllTest {
}
- CalculatorTest.class、SquareTest.class 是测试类的名字。
- 运行结果如下
6. 使用 JUnit 的注意事项
- 测试类和测试方法应该有一致的命名方案。
- 确保测试与时间无关,不要依赖使用过期的数据进行测试,导致在随后的维护过程 中很难重现测试。
- 测试(测试量或代码规模)要尽可能地小,执行速度快。
- 不要硬性规定数据文件的路径。
- 利用 JUnit 的自动异常处理书写简洁的测试代码。
- 事实上在 JUnit 中使用 try-catch 来捕获异常是没有必要的,JUnit 会自动捕获 异常,那些没有被捕获的异常就被当成错误处理。