编写自动化测试

1. 在您开始之前

本 Codelab 将向您介绍 Android 中的自动化测试,以及它们如何帮助您编写可扩展且健壮的应用。您还将更熟悉 UI 逻辑和业务逻辑之间的区别,以及如何测试两者。最后,您将学习如何在 Android Studio 中编写和运行自动化测试。

先决条件

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

您将学到什么

  • Android 中的自动化测试的作用。
  • 为什么自动化测试很重要。
  • 本地测试是什么以及它的用途。
  • Instrumentation 测试是什么以及它的用途。
  • 如何为 Android 代码编写本地测试。
  • 如何为 Android 应用编写 Instrumentation 测试。
  • 如何运行自动化测试。

您将构建什么

  • 本地测试
  • Instrumentation 测试

您需要什么

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 可以随时自动运行本地测试。

Instrumentation 测试

对于 Android 开发,Instrumentation 测试是一种 UI 测试。Instrumentation 测试允许您测试依赖于 Android API 及其平台 API 和服务的应用部分。

与本地测试不同,UI 测试会启动应用或应用的一部分,模拟用户交互,并检查应用是否做出适当的反应。在本课程中,UI 测试将在物理设备或模拟器上运行。

当您在 Android 上运行 Instrumentation 测试时,测试代码实际上会被构建到自己的 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)
}

这使得该方法变为 public,但向其他人表明它仅用于测试目的。

创建测试目录

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

创建**test**目录

  1. 在**Project**选项卡中,将视图更改为 Project。

a6b5eade0103eca9.png

  1. 右键单击**src**目录。

d6bfede787910341.png

  1. 选择**New** > **Directory**。

a457c469e7058234.png

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

bd2c2ef635f0a392.png

  1. 在键盘上按**回车**或**Enter**键。现在可以在**Project**选项卡中看到**test**目录。

d07872d354d8aa92.png

**test**目录需要与包含应用代码的main目录具有相同的包结构。换句话说,就像您的应用代码写入**main > java > com > example > tiptime**包一样,您的本地测试将写入**test > java > com > example > tiptime**包。

在**test**目录中创建此包结构

  1. 右键单击**test/java**目录,然后选择**New > Package**。

99fcf5ff6cda7b57.png

  1. 在**New Package**窗口中,键入com.example.tiptime

6223d2f5664ca35f.png

创建测试类

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

  1. 在**Project**选项卡中,单击**app > src > test**,然后单击d4706c21930a1ef3.png test目录旁边的展开箭头。
  2. 右键单击**com.example.tiptime**目录,然后选择**New > Kotlin Class/File**。

5e9d46922b587fdc.png

  1. 输入TipCalculatorTests作为类名。

9260eb95d7aa6095.png

编写测试

如前所述,本地测试用于测试应用中的小段代码。Tip Time 应用的主要功能是计算小费,因此应该有一个本地测试来确保小费计算逻辑正常工作。

为此,您需要像在应用代码中一样直接调用calculateTip()函数。然后,您确保函数返回的值与根据传递给函数的值得到的预期值匹配。

在编写自动化测试时,您需要了解一些事项。以下概念适用于本地测试和 Instrumentation 测试。起初,它们可能看起来很抽象,但您将在完成此 Codelab 后更加熟悉它们。

  • 以方法的形式编写自动化测试。
  • 使用@Test注解注释该方法。这使编译器知道该方法是一个测试方法,并相应地运行该方法。
  • 确保名称清楚地描述了测试的内容以及预期结果。
  • 测试方法不使用像普通应用方法那样的逻辑。它们不关心某些内容是如何实现的。它们严格检查给定输入的预期输出。也就是说,测试方法仅执行一组指令以断言应用的 UI 或逻辑功能正常。您现在不必理解这意味着什么,因为您稍后会看到它的样子,但请记住,测试代码可能与您习惯的应用代码看起来大不相同。
  • 测试通常以断言结尾,用于确保满足给定条件。断言以具有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.00值的amount变量和一个设置为20.00值的tipPercent变量。
val amount = 10.00
val tipPercent = 20.00
  1. 在应用代码中,在MainActivity.kt文件中,观察以下代码,小费金额根据设备的区域设置进行格式化。

MainActivity.kt

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

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

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

expectedTip变量稍后将与calculateTip()方法的结果进行比较。这就是测试确保方法正常工作的方式。在最后一步中,您将amount变量设置为10.00值,并将tipPercent变量设置为20.00值。10 的 20% 是 2,因此expectedTip变量设置为格式化的货币,其值为2。请记住,calculateTip()方法返回格式化的String值。

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

在这种情况下,您无需考虑舍入,因为预期结果不考虑舍入。

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

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

进行断言通常是自动化测试的最终目标,它不是应用代码中常用的东西。在这种情况下,您希望确保actualTip变量等于expectedTip变量。JUnit库中的assertEquals()方法可用于此目的。

assertEquals()方法采用两个参数——预期值和实际值。如果这些值相等,则断言和测试通过。如果它们不相等,则断言和测试失败。

  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)
    }
}

运行测试

现在是时候运行您的测试了!

您可能已经注意到,在类名和测试函数的行号旁的装订线中出现了箭头。您可以单击这些箭头来运行测试。当您单击方法旁边的箭头时,您只会运行该测试方法。如果在一个类中有多个测试方法,您可以单击该类旁边的箭头来运行该类中的所有测试方法。

722bf5c7600bc004.png

运行测试

  • 单击类声明旁边的箭头,然后单击**Run ‘TipCalculatorTests’**。

a294e77a57b0bb0a.png

您应该会看到以下内容

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

c97b205fef4da587.png

5. 编写 Instrumentation 测试

创建 Instrumentation 目录

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

  1. 右键单击**src**目录,然后选择**New > Directory**。

309ea2bf7ad664e2.png

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

7ad7d6bba44effcc.png

  1. 在键盘上按**回车**或**Enter**键。现在可以在**Project**选项卡中看到**androidTest**目录。

bd0a1ed4d803e426.png

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

  1. 右键单击**androidTest/java**文件夹,然后选择**New > Package**。
  2. 在**New Package**窗口中,键入com.example.tiptime
  3. 在键盘上按**回车**或**Enter**键。现在可以在**Project**选项卡中看到androidTest目录的完整包结构。

创建测试类

在 Android 项目中,Instrumentation 测试目录被指定为androidTest目录。

要创建 Instrumentation 测试,您需要重复创建本地测试时使用的相同过程,但这次在androidTest目录内创建它。

创建测试类

  1. 在项目窗格中导航到androidTest目录。

  2. 点击每个目录旁边的cf54f6c094aa8fa3.png展开箭头,直到看到tiptime目录。

14674cbab3cba3e2.png

  1. 右键单击tiptime目录,然后选择新建 > Kotlin 类/文件
  2. 输入TipUITests作为类名。

acd0c385ae834a16.png

编写测试

Instrumentation 测试代码与本地测试代码有很大不同。

Instrumentation 测试会测试应用程序及其 UI 的实际实例,因此必须设置 UI 内容,类似于在编写 Tip Time 应用程序代码时,在MainActivity.kt文件的onCreate()方法中设置内容的方式。在为使用 Compose 构建的应用程序编写所有 Instrumentation 测试之前,您需要执行此操作。

对于 Tip Time 应用程序测试,您将继续编写与 UI 组件交互的指令,以便通过 UI 测试小费计算过程。Instrumentation 测试的概念起初可能看起来很抽象,但不用担心!接下来的步骤中将介绍此过程。

编写测试

  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注解注释的方法是指 Instrumentation 测试,而在test目录中使用@Test注解注释的方法是指本地测试。

  1. 在函数体中,调用composeTestRule.setContent()函数。这将设置composeTestRule的 UI 内容。
  2. 在函数的 lambda 体中,调用TipTimeTheme()函数,该函数带有一个 lambda 体,用于调用TipTimeLayout()函数。
import com.example.tiptime.ui.theme.TipTimeTheme

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

完成后,代码应类似于在MainActivity.kt文件的onCreate()方法中编写的设置内容的代码。现在 UI 内容已设置好,您可以编写与应用程序 UI 组件交互的指令。在此应用程序中,您需要测试该应用程序是否根据账单金额和小费百分比输入显示正确的小费值。

  1. 可以通过composeTestRule将 UI 组件作为节点进行访问。一种常见的方法是使用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. 使用相同的方法使用20值填充小费百分比的OutlinedTextField
@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 的 Instrumentation 测试中,可以直接在 UI 组件上调用断言。可以使用多种断言,但在本例中,您需要使用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."
   )
}

运行测试

运行 Instrumentation 测试的过程与本地测试相同。您可以单击每个声明旁边的代码行槽中的箭头以运行单个测试或整个测试类。

ad45b3e8730f9bf2.png

  • 单击类声明旁边的箭头。您可以在设备或模拟器上看到测试运行。测试完成后,您应该会看到此图像中显示的输出。

bfd75ec0a8a98999.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 应用程序时,请确保在应用程序功能旁边编写测试,以确保您的应用程序在整个开发过程中都能正常工作。

摘要

  • 什么是自动化测试。
  • 为什么自动化测试很重要。
  • 本地测试和 Instrumentation 测试之间的区别
  • 编写自动化测试的基本最佳实践。
  • 在 Android 项目中查找和放置本地测试类和 Instrumentation 测试类的位置。
  • 如何创建测试方法。
  • 如何创建本地测试类和 Instrumentation 测试类。
  • 如何在本地测试和 Instrumentation 测试中进行断言。
  • 如何使用测试规则。
  • 如何使用ComposeTestRule启动带有测试的应用程序。
  • 如何在 Instrumentation 测试中与可组合项交互。
  • 如何运行测试。