软件设计与数据结构

Chapter 1 Introduction

| 关于   «  3. Java 基础   ::   目录   ::   5. 代码风格与文档:引言  »

4. Java 单元测试

4.1. 目标

完成本模块后,学生将能够:

  • 复习 Java 类的基础知识,包括字段、构造函数、方法、参数以及 this 关键字的使用。

  • 复习调试代码和代码覆盖率。

  • 实现 JUnit 断言语句的变体。

4.2. 交互式:Hokie 班级入门

在本讨论中,我们将通过一个名为"Hokie Class"的示例类来重新审视良好的测试实践。

Follow Along, Practice and Explore

下载并运行 Java 文件(见下文)以在 Eclipse 中自行探索,您可以下载此示例的独立 *.java 文件。要运行独立 *.java 文件,您需要
  1. 创建一个新的 Eclipse 项目,然后

  2. 在项目内创建一个名为"example"的包(类顶部指定的包名必须与文件在 Eclipse 项目中的放置位置包名一致),最后

  3. 下载并导入独立 *.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 方法的工作原理。

Assertions

任务

AssertThat 风格

传统风格

备注

检查 x 是否等于 5

assertThat(x).isEqualTo(5);

assertEquals(5, x);

虽然新风格有 .isNotEqualTo() ,但旧风格中没有 assertNotEquals()

检查一个双精度浮点数 x 是否等于双精度浮点数 y

assertThat(x).isEqualTo(y, within(0.01));

assertEquals(y, x, 0.01);

检查 x 是否等于 true

assertThat(x).isTrue();

assertTrue(x);

检查 x 是否等于 false

assertThat(x).isFalse();

assertFalse(x);

检查 x 是否等于 null

assertThat(x).isNull();

assertNull(x);

检查 x 是否 不 等于 null

assertThat(x).isNotNull();

assertNotNull(x);

检查两个对象变量是否引用内存中的同一空间

assertThat(obj1).isSameAs(obj2);

assertSame(obj2, ob1);

4.5. 交互式:霍基大学 JUnit 测试

Follow Along and Engage

下载对应视频的幻灯片。观看视频时,在幻灯片上做笔记,练习自己绘制图表!

JavaUnitTesting.pdf

4.6. 检查点 2

4.7. 使用学生 TestCase 编写 JUnit 测试的回顾

4.7.1. 使用 JUnit

在 eclipse 中创建 JUnit 测试类:

  1. 右键单击你要创建测试类的类

  2. 点击: New > Class (创建 JUnit 测试用例不符合 CS2-Support 规范)

  3. 命名类 Test。(例如:HokieTest、ArrayBagTest)

  4. 点击完成(如果您希望生成注释,请勾选“生成注释”复选框)

  5. 添加一个导入语句:import student.TestCase

  6. 添加说明,你的类继承自 TestCase。

  7. 项目构建路径应配置为包含 CS2-Support 项目(注意:需要打开 CS2-Support 项目才能将其显示为选项)

  8. 声明实例变量

    • 创建至少一个您正在测试的类的对象的字段。

  9. 编写 setUp 方法

  10. 使用 setUp() 方法初始化您的对象,它将在每个测试方法运行之前执行。

  11. 为被测类中的每个方法编写测试方法

    • 为类中的每个方法至少创建一个测试方法。你的测试类中的每个方法必须以"test"开头,否则它将无法正确运行!(例如:testGetName、testAdd)对于测试方法,调用对象上的相应方法,并使用断言语句来测试你的代码。

  12. 根据需要编写额外的测试方法。Student 类的一个简化测试类示例:

4.7.2. 运行 JUnit 测试

运行 JUnit 测试类:

  1. 右键点击 Package Explorer 中的测试类

  2. 点击: 运行 > 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 (见下图)。

_images/eclipse_failure_trace.png

不能 使用:

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 循环内的逻辑,以及一个单独的测试用例来处理其外的条件语句。这样,如果其中一个失败,您就知道确切地要在代码的哪里查找!

4.8. 编写 JUnit 测试的额外参考资料:

4.9. 检查点

   «  3. Java 基础   ::   目录   ::   5. 代码风格与文档:引言  »

关闭窗口