编写自动化测试

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 应用一样构建到其自己的 Android Application Package (APK) 中。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. 项目标签页中,将视图更改为“项目”。

a6b5eade0103eca9.png

  1. 右键点击 src 目录。

d6bfede787910341.png

  1. 选择 New > Directory

a457c469e7058234.png

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

bd2c2ef635f0a392.png

  1. 按下键盘上的 returnenter 键。现在可以在项目标签页中看到 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. 项目标签页中,点击 app > src > test,然后点击 d4706c21930a1ef3.png 展开箭头旁边的 test 目录。
  2. 右键点击 com.example.tiptime 目录,然后选择 New > Kotlin Class/File

5e9d46922b587fdc.png

  1. 输入 TipCalculatorTests 作为类名。

9260eb95d7aa6095.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() 方法中,创建两个常量变量:一个 amount 变量,设置为 10.00 值;一个 tipPercent 变量,设置为 20.00 值。
val amount = 10.00
val tipPercent = 20.00
  1. 在应用代码中,查看 MainActivity.kt 文件中的以下代码,小费金额根据设备的区域设置进行了格式化。

MainActivity.kt

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

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

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

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

您应该看到以下内容

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

c97b205fef4da587.png

5. 编写插桩测试

创建插桩目录

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

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

309ea2bf7ad664e2.png

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

7ad7d6bba44effcc.png

  1. 按下键盘上的 returnenter 键。现在可以在项目标签页中看到 androidTest 目录了。

bd0a1ed4d803e426.png

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

  1. 右键点击 androidTest/java 文件夹,然后选择 New > Package.
  2. New Package 窗口中,输入 com.example.tiptime
  3. 按下键盘上的 returnenter 键。现在可以在项目标签页中看到 androidTest 目录的完整软件包结构了。

创建测试类

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

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

创建测试类

  1. 导航到项目窗格中的 androidTest 目录。
  2. 点击 cf54f6c094aa8fa3.png 展开箭头,直到看到 tiptime 目录。

14674cbab3cba3e2.png

  1. 右键点击 tiptime 目录,然后选择 New > Kotlin Class/File
  2. 输入 TipUITests 作为类名。

acd0c385ae834a16.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 主体中,调用 TipTimeTheme() 函数,并在其 lambda 主体中调用 TipTimeLayout() 函数。
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. 使用相同的方法,使用值 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 进行插桩测试时,可以直接对界面组件调用断言。有许多断言可用,但在本例中,您希望使用 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."
   )
}

运行测试

运行插桩测试的过程与本地测试相同。您可以点击声明旁边的侧边栏中的箭头来运行单个测试或整个测试类。

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

总结

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