4. Java 单元测试¶
4.1. 目标¶
完成本模块后,学生将能够:
复习 Java 类的基础知识,包括字段、构造函数、方法、参数以及 this 关键字的使用。
复习调试代码和代码覆盖率。
实现 JUnit 断言语句的变体。
4.2. 交互式:Hokie 班级入门¶
在本讨论中,我们将通过一个名为"Hokie Class"的示例类来重新审视良好的测试实践。
Follow Along, Practice and Explore
- 下载并运行 Java 文件(见下文)以在 Eclipse 中自行探索,您可以下载此示例的独立 *.java 文件。要运行独立 *.java 文件,您需要
创建一个新的 Eclipse 项目,然后
在项目内创建一个名为"example"的包(类顶部指定的包名必须与文件在 Eclipse 项目中的放置位置包名一致),最后
下载并导入独立 *.java 文件(或多个文件)到创建的包中。
Hokie.java (right click-> save link as...)
4.3. 检查点 1¶
4.4. Hokie 类 JUnit 测试入门¶
4.4.1. 关于断言语句的说明¶
到目前为止,在课程中,当我们想测试一段代码是否按我们期望的那样运行时,我们会运行像这样的语句:
assertThat(<something we want to check>).isEqualTo(<expected value>);
这是一种更现代的样式,旨在提高可读性。但是,您可以使用另一种语法形式来创建断言:
assertEquals(<expected value>, <something we want to check>);
第二种断言语句如今更为常用,但使用不当可能很棘手。在使用 assertEquals 时,容易将想要检查的值放在前面,而将期望的值放在后面。
例如,假设我们想检查变量 x 是否等于 5。
int x = 4;
assertEquals(x, 5);
这样写作虽然在语法上是正确的,但可能会造成混淆,因为错误消息会显示“期望 [4] 但得到 [5]"。实际上,我们 期望 的是 5,但 得到 的是 4。
课程后半部分的视频将使用这种更常用的第二种语法。您可以继续使用任一版本。下表列出了两种风格的断言。请记住,isEqualto() 和 assertEquals() 方法都使用 equals 方法处理对象参数,务必理解所比较对象的对应 equals 方法的工作原理。
任务 |
AssertThat 风格 |
传统风格 |
备注 |
|---|---|---|---|
检查 |
|
|
虽然新风格有 |
检查一个双精度浮点数 |
|
|
|
检查 |
|
|
|
检查 |
|
|
|
检查 |
|
|
|
检查 |
|
|
|
检查两个对象变量是否引用内存中的同一空间 |
|
|
4.5. 交互式:霍基大学 JUnit 测试¶
4.6. 检查点 2¶
4.7. 使用学生 TestCase 编写 JUnit 测试的回顾¶
4.7.1. 使用 JUnit¶
在 eclipse 中创建 JUnit 测试类:
右键单击你要创建测试类的类
点击: New > Class (创建 JUnit 测试用例不符合 CS2-Support 规范)
命名类 Test。(例如:HokieTest、ArrayBagTest)
点击完成(如果您希望生成注释,请勾选“生成注释”复选框)
添加一个导入语句:import student.TestCase
添加说明,你的类继承自 TestCase。
项目构建路径应配置为包含 CS2-Support 项目(注意:需要打开 CS2-Support 项目才能将其显示为选项)
声明实例变量
创建至少一个您正在测试的类的对象的字段。
编写 setUp 方法
使用 setUp() 方法初始化您的对象,它将在每个测试方法运行之前执行。
为被测类中的每个方法编写测试方法
为类中的每个方法至少创建一个测试方法。你的测试类中的每个方法必须以"test"开头,否则它将无法正确运行!(例如:testGetName、testAdd)对于测试方法,调用对象上的相应方法,并使用断言语句来测试你的代码。
根据需要编写额外的测试方法。Student 类的一个简化测试类示例:
4.7.2. 运行 JUnit 测试¶
运行 JUnit 测试类:
右键点击 Package Explorer 中的测试类
点击: 运行 > JUnit 测试 一个 JUnit 窗口应该弹出,如果所有测试都正确则显示绿色,如果有一个失败则显示红色。
4.7.3. 命名约定¶
对于类:将测试添加到类名的末尾
示例:HelloWorld 是类;HelloWorldTest 是测试类
对于方法:用测试方法开始测试
示例:foo 是方法;testFoo 是测试方法
4.7.4. 实例变量¶
使用实例变量来存储用于测试的值
AKA 域变量,成员变量
实例变量的作用域是所有实例方法,因此该变量可在多个测试用例中使用。
在上述例子中, janeDoe 是一个实例变量
4.7.5. 初始化方法¶
setUp() 方法在每个测试方法之前运行。
使用此方法初始化实例变量
必须调用 setUp – 记得将其首字母大写!
4.7.6. 代码覆盖率¶
编写测试平均情况的测试用例
示例:在列表中测试向中间添加
编写测试边缘情况的测试用例
示例:在列表中测试在列表开头添加
4.7.7. N 个简单条件,N+1 个分支和测试¶
测试方法中的断言需要覆盖 if-else 语句的每个条件。仅仅测试到达'else'条件是不够的。要正确测试 if-else 语句,每个条件的代码体必须在测试期间运行。
if (x == 0 && y ==1) // 2 conditions, 3 checks- TF, FT, TT
if (x == 0 || y == 1) // 2 conditions, 3 checks- TF, FT, FF
关于边界情况和平均情况的说明 - 对于一个包含 100 个值的列表,您必须检查索引 -1、0、99、100 以及中间某个值。
示例:假设我们有以下情况:
您的测试类需要测试上述 5 种可能性,以便执行 if-else 语句块中的每一行代码。
有时测试代码的最佳方式是先清理代码!
在测试代码之前清理代码可以节省大量时间。此外,您组织代码的方式可能使其更容易被正确测试。
示例:假设我们在方法内部写了以下内容:
if ( A > B )
{
if ( C != 0 && ( A > B ))
{
// do something
}
}
我们可以很容易地通过注意到我们重复评估了 A > B 这一事实来清理这个 if 语句,这是不必要的。我们可以将其重写为以下形式:
if ( A > B )
{
if ( C != 0)
{
// do something
}
}
我们可能决定同样地取消嵌套:
if ( (A > B) && ( C != 0) )
{
//do something
}
现在,更容易看到所有需要测试的条件。
4.7.8. 简化测试¶
在测试包含多个 if-else 语句的方法时,通常可以将每个可能性简化为独立的测试方法。
假设我们正在测试包含如下 if-else 语句的方法:
if ( A > B)
{
//do something
}
else
{
//do something else
}
可能有一个好主意,即编写一个测试方法在 A > B 为真时评估该 if 语句,并编写另一个测试方法在 A > B 为假时评估相同的 if 语句。
4.7.9. 检查点 3¶
4.7.10. 测试异常¶
如果你抛出它们,然后在测试中捕获它们!
在测试中使用 try-catch 块来检查您的代码是否抛出了正确的异常。在您的 try 块中,您应该调用导致抛出异常的该方法。catch 块应该捕获抛出的异常。然后断言该异常存在、是正确类型的异常,并且(如果适用)包含正确的消息。
示例:假设你试图访问一个无法通过迭代器对象访问的数据结构中的元素,因此你正在测试检查是否抛出 NoSuchElementException 异常,消息为"There are no more elements left to iterate over."。以下测试方法内部将确定你是否正确捕获了该异常:
示例:
Exception thrown = null;
try
{
//call the method that should throw a NoSuchElementException
iterate.next();
}
catch (Exception exception)
{
//”Catch” and store the exception
thrown = exception;
}
//assert that an exception was thrown
assertNotNull(thrown);
//assert that the correct exception was thrown
assertTrue(thrown instanceof NoSuchElementException);
//Check the message of the exception is correct
assertEquals(thrown.getMessage(), "There are no more elements left to iterate over.");
4.7.11. 检查点 4¶
4.7.12. 测试 toArray() 方法¶
toArray() 方法返回一个包含给定集合中每个元素的 Object 数组。
测试 toArray() 方法需要确认该方法返回的实际对象数组与预期的对象数组相匹配。
注意, assertEquals 和 assertTrue 方法不提供一种方便比较两个数组的机制,因为数组没有定义 equals 方法。我们 CANNOT 仅仅执行以下操作:
Object[] expectedArray = {"A","B","C","D"};
Object[] actualArray = {"A","B","C","D"};
assertEquals(expectedArray, actualArray);
使用这种方式调用 assert 会导致测试失败并抛出 AssertionFailedError (见下图)。
不能 使用:
assertTrue( expectedArray.equals( actualArray) );
因此我们需要一个替代方案。
一种方法是遍历每个数组的元素,将其中一个数组的每个元素与另一个数组的对应元素进行比较。如果任何一对元素不匹配,则可以得出结论:这两个数组不相等,因此返回 false。
考虑使用 for 循环来帮助完成此类任务。
4.7.13. 检查点 5¶
4.7.14. 通用 JUnit 测试建议¶
调试一个损坏的测试用例可能很繁琐,尤其是在大型项目中。为了让自己更容易,请确保每个测试用例恰好覆盖一个逻辑组件。例如,让我们考虑我们 Hokie 类的这种简化形式:
public class Hokie {
private String pid;
private String hometown;
private int graduationYear;
private int DOBYear;
public boolean setDOBYear(int year) {
if (year > 0 && (year < 3000)) {
DOBYear = year;
return true;
}
return false;
}
public String toString() {
return pid;
}
}
我们可以创建一个像这样的测试用例:
public void test1(){
// Tests setDOBYear
assertTrue(elena.setDOBYear(1968));
assertEquals(1968,elena.getDOBYear());
assertFalse(john.setDOBYear(12031995));
// tests toString
Hokie gobbler = new Hokie("gobbledee",1973);
assertEquals("gobbledee",gobbler.toString());
}
public void test1(){
// Tests setDOBYear
assertTrue(elena.setDOBYear(1968));
assertEquals(1968,elena.getDOBYear());
assertFalse(john.setDOBYear(12031995));
// tests toString
Hokie gobbler = new Hokie("gobbledee",1973);
assertEquals("gobbledee",gobbler.toString());
}
然而,如果 test1 失败,为了调试它,你现在必须考虑测试中可能存在错误,或者 setDOBYear() 方法中可能存在错误,或者 getDOBYear() 方法中可能存在错误,或者 toString() 方法中可能存在错误。Eclipse 会指引你到出错的行,但这并不总是能告诉你问题实际上是从哪里开始的!无论如何,为代码的一个且仅一个逻辑组件编写测试方法是良好的实践。将这两个部分拆分为单独的测试将使未来的调试工作更容易。
在更大的程序中,仅对每个方法编写 1 个测试用例可能还不够。考虑以下代码:
public int foo(int x, int y){
for (int i = 0; i < 10; i++){
x+=i;
if (x % 3 == 0){
x++;
}
y *= i;
}
if (x % 2 == 0){
return x;
}else if (y % 2 == 0){
return y;
}
return 0;
}
您可能更容易编写一个测试用例来处理 for 循环内的逻辑,以及一个单独的测试用例来处理其外的条件语句。这样,如果其中一个失败,您就知道确切地要在代码的哪里查找!
