编写自动化测试

1. 准备工作

在此 Codelab 中,您将了解 Android 中的自动化测试,以及如何借助自动化测试来编写可伸缩的可靠应用。此外,您还将更熟悉界面逻辑与业务逻辑之间的区别,以及如何测试这两种逻辑。最后,您将学习如何在 Android Studio 中编写和运行自动化测试。

前提条件

  • 能够使用函数和可组合项编写 Android 应用。

学习内容

  • Android 中的自动化测试有何功能。
  • 自动化测试的重要性何在。
  • 本地测试的定义和用途是什么。
  • 插桩测试的定义和用途是什么。
  • 如何为 Android 代码编写本地测试。
  • 如何为 Android 应用编写插桩测试。
  • 如何运行自动化测试。

构建内容

  • 一项本地测试
  • 一项插桩测试

所需条件

2. 获取起始代码

下载代码:

或者,您也可以克隆该代码的 GitHub 代码库:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout main

3. 自动化测试

就软件而言,测试是一种检查软件以确保其按预期运行的系统化方法。自动化测试是一段代码,用于检查您所编写的另一段代码,确保其能正常运行。

测试是应用开发流程的一个重要环节。通过持续对应用运行测试,您可以在公开发布应用之前验证其正确性、功能行为和易用性。

通过测试,您还可以在引入更改时持续检查现有代码。

虽然手动测试几乎总是有它的一席之地,但 Android 中的测试通常可以自动执行。在课程的剩余部分,您将重点学习使用自动化测试来测试应用代码和应用本身的功能要求。在此 Codelab 中,您将了解关于 Android 测试的基础知识。在后续 Codelab 中,您将学习有关测试 Android 应用的更多高级做法。

随着您对 Android 开发和 Android 应用测试的熟悉,您应该将在编写应用代码的同时编写测试作为一种常规做法。如果每次在应用中创建新功能时都创建一个测试,可以在以后应用变大时减少工作量。这样做还可以让您无需花费太多时间来手动测试应用,从而便捷地确保应用能够正常运行。

自动化测试是所有软件开发中不可或缺的一部分,Android 开发也不例外。因此,现在就是介绍它的最佳时机!

自动化测试的重要性何在

乍看之下,应用中似乎并非一定要包含测试,但所有规模和复杂程度的应用中都需要包含测试。

如需扩展代码库,您需要在添加新代码时测试现有功能,而这只有在您拥有现有测试的情况下才能实现。随着应用体量增加,手动测试的工作量会远远超过自动化测试的工作量。此外,如果您开始开发正式版应用,并拥有大量用户,那么测试就变得至关重要。例如,您必须考虑运行许多不同 Android 版本的各种不同类型的设备。

最终您会发现,自动化测试能够以远超手动测试的速度考虑绝大多数使用场景。如果您在发布新代码之前运行测试,就可以对现有代码做出必要的更改,以免发布的应用出现意外行为。

请注意,自动化测试是通过软件执行的测试,手动测试则与之相反,是由人直接与设备互动来进行测试。自动化测试和手动测试对于确保产品用户获得愉快的体验至关重要。不过,自动化测试不仅准确性更高,还能提高团队的工作效率(因为不需要分派人手运行测试),而且执行速度也比手动测试要快得多。

自动化测试的类型

本地测试

本地测试是一种自动化测试,可直接测试一小段代码,以确保其能够正常运行。借助本地测试,您可以测试函数、类和属性。本地测试在工作站上执行,这意味着它们在开发环境中运行,不需要使用实体设备或模拟器。这是在计算机上运行本地测试的一大亮点。在计算机资源方面,本地测试的开销也非常低,因此即使资源有限,此类测试也可以快速运行。Android Studio 已准备好自动运行本地测试。

插桩测试

在 Android 开发环境中,插桩测试是一种界面测试。借助插桩测试,您可以测试应用中依赖于 Android API 及其平台 API 和服务的部分。

与本地测试不同,界面测试会启动应用的全部或部分内容、模拟用户互动,并检查应用响应是否适当。在此课程中,界面测试均在实体设备或模拟器上运行。

在 Android 上运行插桩测试时,测试代码实际上会内置到其自己的 Android 应用软件包 (APK) 中,就像常规 Android 应用一样。APK 是一个压缩文件,其中包含了在设备或模拟器上运行应用所需的所有代码和文件。该测试 APK 将与常规应用 APK 一起安装在设备或模拟器上。然后,测试 APK 会针对应用 APK 运行测试。

4. 编写本地测试

准备应用代码

本地测试会直接测试应用代码中的方法,因此要测试的方法必须对测试类和方法可用。以下代码段中的本地测试可确保 calculateTip() 方法能够正常运行,但 calculateTip() 方法目前处于 private 状态,因此无法从测试访问。请移除 private 标识并将其变为 internal

MainActivity.kt

internal fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}
  • MainActivity.kt 文件中 calculateTip() 方法前面的代码行中,添加 @VisibleForTesting 注解:
@VisibleForTesting
internal fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}

这会将该方法设为公开状态,但会向其他元素指明该方法仅为测试目的而公开。

创建 test 目录

在 Android 项目中,test 目录是编写本地测试的位置。

创建 test 目录:

  1. Project 标签页中,将视图更改为“Project”。

b9fac49a80bc59f6.png

  1. 右键点击 src 目录。

6cdf1a84fd2c0a25.png

  1. 选择 New

dc9d7b82d65502a3.png

  1. 选择 Directory

1c9115800a6f8e36.png

  1. New Directory 窗口中,选择 test/java

56f5e2df9525a230.png

  1. 按键盘上的 ReturnEnter 键。现在,Project 标签页中会显示 test 目录。

60c6a44570332cab.png

test 目录要求的应用软件包结构与应用代码所在的 main 目录相同。也就是说,就像您的应用代码会写入 main > java > com > example > tiptime 中一样,您的本地测试将写入 test > java > com > example > tiptime

test 目录中创建此软件包结构:

  1. 右键点击 test/java 目录,然后依次选择 New > Package

5814cfecbebd43e1.png

  1. New Package 窗口中,输入 com.example.tiptime

74fc5fbc7e051a4c.png

创建测试类

现在,test 软件包已准备就绪,是时候编写一些测试了!首先,创建测试类。

  1. Project 标签页中,依次点击 app > src > test,然后点击 test 目录旁边的 7aeb5945d20f0dd0.png 展开箭头。

显示 unitTest 文件夹

  1. 右键点击 tiptime 目录,然后依次选择 New > Kotlin Class/File

8c64ee6e43c62481.png

  1. 输入 TipCalculatorTests 作为类名称。

8c39d1d2ac201307.png

编写测试

如前所述,本地测试用于测试应用中的小段代码。Tip Time 应用的主函数用于计算小费,因此应该设置一项本地测试来确保小费计算逻辑能够正常运行。

为了实现这一点,您需要直接调用 calculateTip() 函数,如您在应用代码中所做的那样。然后,您要确保该函数返回的值与基于您传递到该函数的值而预期得到的值一致。

关于编写自动化测试,有几点需要您注意。下面列出的概念适用于本地测试和插桩测试。这些概念乍一看似乎比较抽象,但在此 Codelab 结束时,您会更熟悉它们。

  • 以方法的形式编写自动化测试。
  • 使用 @Test 为方法添加注解。这样一来,编译器就能知道相应方法属于测试方法,从而相应地运行该方法。
  • 确保名称能够清楚地说明测试的测试内容和预期结果。
  • 测试方法使用逻辑的方式与常规应用方法不同。测试方法不关注内容的实现方式。它们会严格检查给定输入的预期输出。也就是说,测试方法仅执行一组指令以断言应用的界面或逻辑能够正常运行。目前,您不必理解上述说法的含义,因为稍后您就会看到实际运作方式,但请注意,测试代码看起来可能与您所熟悉的应用代码有很大不同。
  • 测试通常以“断言”结尾,该断言用于确保满足指定条件。断言采用方法调用的形式,其名称中包含“assert”。例如:assertTrue() 是 Android 测试中常用的断言。断言语句在大多数测试中都会用到,但在实际应用代码中却极少用到。

编写测试:

  1. 创建一个方法,以便测试如果账单金额为 10 美元,20% 的小费是多少。预期计算结果为 2 美元。
import org.junit.Test

class TipCalculatorTests {

   @Test
   fun calculateTip_20PercentNoRoundup() {

   }
}

您可能还记得,应用代码中的 MainActivity.kt 文件中的 calculateTip() 方法需要三个形参,账单金额、小费百分比以及是否舍入结果的标志。

fun calculateTip(amount: Double, tipPercent: Double, roundUp: Boolean)

从测试中调用该方法时,需要像在应用代码中调用该方法时一样传递这些形参。

  1. calculateTip_20PercentNoRoundup() 方法中,创建两个常量变量:一个值设为 10.00amount 变量和一个值设为 20.00tipPercent 变量。
val amount = 10.00
val tipPercent = 20.00
  1. 在应用代码的 MainActivity.kt 文件中,观察以下代码,小费金额的格式取决于设备的语言区域。

MainActivity.kt

...
NumberFormat.getCurrencyInstance().format(tip)
...

在测试中验证预期的小费金额时,必须使用相同的格式。

  1. 创建一个设置为 NumberFormat.getCurrencyInstance().format(2)expectedTip 变量。

稍后,系统会对 expectedTipcalculateTip() 方法的结果进行比较。测试就是这样确保方法能够正常运行的。在最后一步中,您将 amount 变量的值设为 10.00,并将 tipPercent 变量的值设为 20.00。10 的 20% 是 2,因此要将 expectedTip 变量设成值为 2 的格式化货币价格。请注意,calculateTip() 方法会返回格式化的 String 值。

  1. 使用 amounttipPercent 变量调用 calculateTip() 方法,并为舍入传递一个 false 实参。

在本例中,您不需要考虑向上舍入,因为预期结果并未考虑向上舍入。

  1. 将方法调用的结果存储在常量 actualTip 变量中。

到目前为止,编写该测试的方式与在应用代码中编写常规方法没有太大区别。不过,现在您已从要测试的方法获得了返回值,接下来就必须使用断言来确定该值是否为正确的值。

进行断言通常是自动化测试的最终目标,在应用代码中这并不常用。在本例中,您要确保 actualTip 变量等于 expectedTip 变量。为此,可以使用 JUnit 库中的 assertEquals() 方法。

assertEquals() 方法接受 2 个形参:一个预期值和一个实际值。如果这些值相等,则断言和测试通过。如果不相等,则断言和测试失败。

  1. 调用该 assertEquals() 方法,然后传入 expectedTipactualTip 变量作为形参:
import org.junit.Assert.assertEquals
import org.junit.Test
import java.text.NumberFormat

class TipCalculatorTests {

    @Test
    fun calculateTip_20PercentNoRoundup() {
        val amount = 10.00
        val tipPercent = 20.00
        val expectedTip = NumberFormat.getCurrencyInstance().format(2)
        val actualTip = calculateTip(amount = amount, tipPercent = tipPercent, false)
        assertEquals(expectedTip, actualTip)
    }
}

运行测试

现在该运行测试了!

您可能已经注意到,类名称和测试函数的行号旁边的边线中会显示箭头。您可以点击这些箭头来运行测试。如果点击某个方法旁的箭头,您只会运行该测试方法。如果您在某个类中包含多个测试方法,则可以点击该类旁边的箭头,运行此类中的所有测试方法。

d1d3291589b08b74.png

运行测试:

  • 点击类声明旁的箭头,然后点击 Run 'TipCalculatorTests'

301a67db81194d1a.png

您应该会看到以下内容:

  • 在边线中,箭头被替换为绿色的对勾标记和三角形 dc22757efa3bff97.png。这表示已通过测试。

ecf625f23f30a1bb.png

  • Run 窗格的底部,您会看到一些输出。

显示已通过测试

  • 说明已通过测试的指示内容。

5. 编写插桩测试

创建插桩目录

创建插桩目录的方式与本地测试目录类似。

  1. 右键点击 src 目录,然后依次选择 New > Directory

已选择目录菜单选项

  1. New Directory 窗口中,选择 androidTest/java

49b436219213c56d.png

  1. 按键盘上的 ReturnEnter 键。现在,Project 标签页中会显示 androidTest 目录。

已选择 androidTest 文件夹

就像 maintest 目录具有相同的软件包结构一样,androidTest 目录也必须包含相同的软件包结构。

  1. 右键点击 androidTest/java 文件夹,然后依次选择 New > Package
  2. New Package 窗口中,输入 com.example.tiptime
  3. 按键盘上的 ReturnEnter 键。现在,Project 标签页中会显示 androidTest 目录的完整软件包结构。

创建测试类

在 Android 项目中,插桩测试目录已指定为 androidTest 目录。

如需创建插桩测试,您需要重复创建本地测试时所用的流程,但这次需要在 androidTest 目录中创建该测试。

创建测试类:

  1. 在“Project”窗格中进入 androidTest 目录。

a627f92d40041107.png

  1. 点击每个目录旁的 a30374584d86ddb6.png 展开箭头,直到看到 tiptime 目录为止。

7653ebbc899a26a.png

  1. 右键点击 tiptime 目录,然后依次选择 New > Kotlin Class/File

69b2c4bcf72c7b1a.png

  1. 输入 TipUITests 作为类名称。

8685533c87fbbea0.png

编写测试

插桩测试代码与本地测试代码完全不同。

插桩测试会测试应用的实际实例及其界面,因此,您必须设置界面内容,类似于您为 Tip Time 应用编写代码时在 MainActivity.kt 文件的 onCreate() 方法中设置的内容。您需要先执行该操作,然后才能为使用 Compose 构建的应用编写所有插桩测试。

对于 Tip Time 应用的测试,您需要继续编写指令以与界面组件交互,从而通过界面测试小费计算过程。插桩测试的概念乍一看似乎比较抽象,但不用担心!以下步骤将详细展示这个流程。

编写测试:

  1. 创建一个 composeTestRule 变量,将其设为 createComposeRule() 方法的结果,并使用 Rule 注解为其添加注解:
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TipUITests {

   @get:Rule
   val composeTestRule = createComposeRule()
}
  1. 创建一个 calculate_20_percent_tip() 方法,并使用 @Test 注解为其添加注解:
import org.junit.Test

@Test
fun calculate_20_percent_tip() {
}

编译器知道 androidTest 目录中带有 @Test 注解的方法引用的是插桩测试,而 test 目录中带有 @Test 注解的方法引用的是本地测试。

  1. 在函数正文中,调用 composeTestRule.setContent() 函数。这样即可设置 composeTestRule 的界面内容。
  2. 在函数的 lambda 正文中,使用调用 TipTimeLayout() 函数的 lambda 正文调用 TipTimeTheme() 函数。
import com.example.tiptime.ui.theme.TipTimeTheme

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
           TipTimeLayout()
        }
    }
}

完成后,代码应类似于为了在 MainActivity.kt 文件的 onCreate() 方法中设置内容而编写的代码。现在界面内容已设置完毕,接下来就可以编写指令来与应用界面组件交互了。在此应用中,您需要测试应用能否根据输入的账单金额和小费百分比来显示正确的小费值。

  1. 您可以通过 composeTestRule 将界面组件作为节点进行访问。为实现该目的,一种常用方法是使用 onNodeWithText() 方法访问包含特定文本的节点。使用 onNodeWithText() 方法访问账单金额的 TextField 可组合项:
import androidx.compose.ui.test.onNodeWithText

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
}

接下来,您可以调用 performTextInput() 方法,并传入要输入以填充 TextField 可组合项的文本。

  1. 使用 10 值填充账单金额的 TextField
import androidx.compose.ui.test.performTextInput

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
}
  1. 使用相同方法向针对小费百分比的 OutlinedTextField 填充 20 值:
@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
   composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
   composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
}

填充所有 TextField 可组合项后,小费将显示在应用屏幕底部的 Text 可组合项中。

现在您已指示测试填充这些 TextField 可组合项,接下来必须使用断言来确保 Text 可组合项能够显示正确的小费。

在使用 Compose 的插桩测试中,可以直接对界面组件调用断言。可用的断言有很多,但在本例中,您要使用 assertExists() 方法。显示小费金额的 Text 可组合项应显示:Tip Amount: $2.00

  1. 断言存在具有相应文本的节点:
import java.text.NumberFormat

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            Surface (modifier = Modifier.fillMaxSize()){
                TipTimeLayout()
            }
        }
    }
   composeTestRule.onNodeWithText("Bill Amount")
      .performTextInput("10")
   composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
   val expectedTip = NumberFormat.getCurrencyInstance().format(2)
   composeTestRule.onNodeWithText("Tip Amount: $expectedTip").assertExists(
      "No node with this text was found."
   )
}

运行测试

运行插桩测试的流程与本地测试相同。点击每个声明旁的边线中的箭头,以运行单个测试或整个测试类。

b435bcafc02c94ef.png

  • 点击类声明旁的箭头。您可以看到相应测试在设备或模拟器上运行。测试完成后,您应该会看到如下图所示的输出:

f878f82d3469e877.png

6. 获取解决方案代码

或者,您也可以克隆 GitHub 代码库:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout test_solution

7. 总结

恭喜!您在 Android 中编写了自己的第一项自动化测试。测试是软件质量控制的关键环节。随着您不断构建 Android 应用,务必要在编写应用功能的同时编写测试,以确保在整个开发过程中应用都能够正常运行。

摘要

  • 什么是自动化测试。
  • 自动化测试的重要性何在。
  • 本地测试和插桩测试的区别。
  • 编写自动化测试的基本最佳实践。
  • 在 Android 项目中的什么位置能够找到且适合放置本地测试类和插桩测试类。
  • 如何创建测试方法。
  • 如何创建本地测试类和插桩测试类。
  • 如何在本地测试和插桩测试中进行断言。
  • 如何使用测试规则。
  • 如何使用 ComposeTestRule 通过测试启动应用。
  • 如何在插桩测试中与可组合项交互。
  • 如何运行测试。