编写单元测试

1. 准备工作

在之前的 Codelab 中,您已学过如何使用 Android Studio 创建项目、如何修改 XML 文件以便为应用构建自定义界面,以及如何修改业务逻辑以添加功能。此 Codelab 重点介绍测试的重要性,并深入介绍了单元测试。您将了解单元测试是什么样的,以及如何编写单元测试。

前提条件

  • 已在 Android Studio 中创建过项目。
  • 具有一定的在 Android Studio 中编写代码的经验。

学习内容

  • 测试为什么重要?
  • 单元测试是什么样的?
  • 如何编写和运行单元测试?

所需条件

  • 一台安装了 Android Studio 的计算机。
  • 您在此在线课程的上一个 Codelab 中创建的项目。

2. 简介

您已经编写过一些 Android 代码,现在您完全可以在此基础上编写一些测试代码。在本课中,您要先重温一些测试理念,然后深入了解 Android 项目中自动生成的测试,最后自行为 Dice Roller 应用编写测试!本课涉及许多资料,但没必要望而生畏!请花点时间学习这些资料,因为学习测试需要付出很多时间并进行大量练习,如果不能马上掌握,也不要气馁。

测试为什么重要?

乍看之下,应用中似乎并非一定要包含测试。如果应用很小且功能有限,我们可以轻松地手动对其进行测试,并确定是否一切正常。不过,随着应用体量增加,手动测试的工作量会远远超过编写自动化测试的工作量。此外,一旦您开始开发专业级应用,当您拥有大量用户之后,测试就变得至关重要。您必须考虑运行许多不同 Android 版本的各种不同类型的设备。最终您会发现,自动化测试能够以远超手动测试的速度考虑绝大多数使用场景。如果您在发布新代码之前运行测试,就可以对现有代码做出必要的更改,以免发布的应用出现意外行为。请注意,自动化测试是通过软件执行的测试,手动测试则与之相反,是由人直接与设备互动来进行测试。自动化测试和手动测试对于确保产品用户获得愉快的体验至关重要。不过,自动化测试不仅准确性更高,还能提高团队的工作效率(因为不需要分派人手运行测试),而且执行速度也比手动测试要快得多。

单元测试详细介绍

在此 Codelab 中,您将着重了解单元测试。稍后,您还会学习插桩测试。首先,您将了解通过 Android Studio 创建 Android 应用时生成的测试。然后,您将通过动手实践,亲自体验如何运行测试并熟悉如何编写测试代码。

在之前的在线课程中,您已了解了如何查找用于测试的源文件。单元测试的相关文件始终位于 test 目录下:

f02b380da4e8f661.png

  1. 打开 app/build.gradle 文件并查看依赖项。您会看到一些标记为 testImplementationandroidTestImplementation 的依赖项,它们分别对应于单元测试和插桩测试。需要注意的是:

app/build.gradle

testImplementation 'junit:junit:4.12'

这是驱动单元测试的 JUnit 库,可让您将代码标记为测试,以便采用可以测试应用代码的方式来编译和运行相关代码。

  1. 打开 test 目录下的 ExampleUnitTest.kt 文件。

您应该会看到如下所示的示例单元测试:

ExampleUnitTest.kt

class ExampleUnitTest {
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
   }
}

虽然您向 Dice Roller 应用中添加了一些代码,但是您可能未编写任何测试。因此,现在除了一些由 Android Studio 自动创建的通用代码之外什么都没有。此测试可为任意测试,它只是充当占位符,代表预期开发者要编写的更符合具体情况的测试。目前,此代码块仅测试 2 + 2 = 4。当然,结果始终应为 true。下面,我们来详细看看测试过程:

  • 首先,必须使用从 org.junit.test 库导入的 @ Test 注解对测试函数进行注解。您可以将注解视为一段代码的元数据标记,它可以改变代码的编译方式。在本例中,@Test 注解让编译器知道跟在其后的方法是一项测试,从而让该方法作为测试运行。

跟在注解后的是一个函数声明,在本例中为 addition_isCorrect() 函数。在该函数内,assertEquals() 函数声明预期值应等于通过业务逻辑获取的实际值。断言方法是单元测试的最终目标。最终,您需要断言从代码获得的结果为某个特定状态。如果结果的状态与预期状态相符,测试就会通过;如果不符,测试就会失败。在本例中,代码要比较两个值,因此 assertEquals() 方法接受预期值和实际值这两个参数。正如名称所言,预期值就是您预期的特定结果,在本例中为 4。实际值表示一段实际代码的结果。通常,此测试会测试应用本身的一段代码。不过,在本例中,我们只测试一段任意代码,例如 2 + 2。现在,言归正传,让我们运行一下这项测试,看看会发生什么情况。

在 Android Studio 中,您可以通过多种方式运行测试,稍后您会深入了解。现在,只需要采取简单的方式。

  1. 点击 addition_isCorrect 方法声明旁的箭头,然后选择 Run ‘ExampleUnitTest.addition_isCorrect'

78c943e851a33644.png

我们将这种检测方法称之为正向检测。换句话说,断言是肯定的。例如:2 + 2 等于 4。或者,我们可以编写一个反向测试,形成否定性的断言。例如:2 + 2 不等于 5。

Run 窗格中,您应该会看到类似以下屏幕截图的内容:

190df0c8ff787233.png

表明测试成功的不同指示包括绿色对勾标记和通过的测试数量。

aa7d361d8e4826ef.png

  1. 修改测试,看看测试失败是什么样的。将 2 + 2 更改为 2 + 3,然后再次执行测试。请注意,您只是使用生成的代码进行实验,以便熟悉测试的工作方式。这些更改与 Dice Roller 的功能没有任何关系。

ExampleUnitTest.kt

class ExampleUnitTest {
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 3)
   }
}

运行测试后,您应该会看到类似以下屏幕截图的内容:

751ac8089cf4c47c.png

红色文本表示测试失败。在测试结果的菜单中,点击其中的项目会显示错误消息,说明测试失败的原因。

163708373e651ecc.png

在本例中,错误消息指出断言失败,因为预期结果为 4 而实际值为 5。这说得通,因为您将实际值更改为 2 + 3,但预期值仍保留为 4。您还可以看到导致测试失败的行。在本例中是第 15 行,表示为 ExampleUnitTest.kt:15

  1. 为了测试得更彻底,请将预期值从 4 更改为 5,然后再次运行测试。现在,测试应该会通过,因为相关代码的预期值与实际结果相符。

3. 编写您的首个单元测试

现在,您已经在一定程度上熟悉了单元测试,可以自行编写更贴合 Dice Roller 应用具体情况的单元测试了。

您应该已经注意到,Dice Roller 应用的主要功能基于一个随机数生成器。不幸的是,人尽皆知,测试随机数生成器非常困难,因为我们不能确定随机生成的数字是什么结果。此测试的目标是确保在掷骰子(即,对 dice 类调用 roll 方法)时得到恰当的数字。您编写的测试只需测试随机数生成器输出的数字是否在您为生成器指定的范围内。

  1. ExampleUnitTest.kt 文件中,删除生成的测试方法和 import 语句。现在,您的文件应如下所示:

c06e8b402f293b5e.png

  1. 创建一个 generates_number() 函数。

ExampleUnitTest.kt

fun generates_number() {
}
  1. 使用 @Testgenerates_number() 方法添加注解。请注意,当您尝试调用 @Test 时,文本为红色。这是因为系统找不到此注解的声明,所以您需要导入此注解。当您按 Control+Enter(在 Mac 上,则按 Options+Return)时,系统会自动执行此操作。

如果您点击代码行,应该会看到一条导入提示:

bbe5791b9565588c.png

或者,您也可以复制 import org.junit.Test 文件并将其粘贴到软件包名称后、类声明前。代码现在应如下所示:

9a94c2bdf84adb61.png

  1. 创建 Dice 对象的实例。

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
}
  1. 接下来,对此实例调用 roll() 方法,并存储返回的值。

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
   val rollResult = dice.roll()
}
  1. 最后,进行实际断言。换言之,您需要断言该方法返回的值在您传入的骰面数字之内。因此,在本例中,此值需要大于 0 且小于 7。为此,您要使用 assertTrue() 方法。请注意,当您尝试调用 assertTrue() 方法时,文本最初为红色。与您调用注解时遇到的情况类似,这是因为系统找不到此方法的声明,所以您需要导入此方法。

10eea07fc21bf998.png

如前所述,您可以自动导入此方法。不过请注意,这一次有多个选项可供您选择。在本例中,应该选择 org.junit.Assert 软件包中的选项:

5dbfba2ba0e37ac9.png

或者,您也可以将此代码粘贴到 test 注解的 import 语句之后:

ExampleUnitTest.kt

import org.junit.Assert.assertTrue

现在,您的代码应如下所示:

347f792f455ae6b5.png

如果将光标移到括号内并按 Control+P(在 Mac 上,则按 Command+P),您会看到一条提示,显示了该方法接受的参数:

865cf0ac47738e08.png

assertTrue() 方法接受两个参数:StringBoolean。如果断言失败,字符串就是控制台中显示的消息。布尔值是一个条件语句。请将消息设为:

ExampleUnitTest.kt

"The value of rollResult was not between 1 and 6"

如前所述,测试随机数是一项挑战,因为随机这一性质导致数字的值无法预测。您能做的只是确保值在某个特定范围内。请将条件参数设为:

ExampleUnitTest.kt

rollResult in 1..6

代码应如下所示:

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
   val rollResult = dice.roll()
   assertTrue("The value of rollResult was not between 1 and 6", rollResult in 1..6)
}
  1. 点击函数旁的箭头,然后选择 Run ‘ExampleUnitTest.generates_number()'

如果您的代码类似于前面的代码段,那么您的测试应该会通过!

  1. 可选:如果想进行额外的练习,请将骰子修改为 4 面或 5 面但不改断言,看看测试失败的情况。

4. 恭喜

您已完成以下内容的学习:

  • 测试的重要性。
  • 单元测试是什么样的?
  • 如何运行单元测试?
  • 一些常见的测试语法。
  • 如何编写单元测试?

了解详情