测试 ViewModel 和 LiveData

1. 准备工作

在之前的 Codelab 中,您已学过如何使用 ViewModel 处理业务逻辑,以及如何使用 LiveData 构建响应式界面。在此 Codelab 中,您将学习如何编写单元测试来检查 ViewModel 代码能否正常运行。

前提条件

  • 已在 Android Studio 中创建过测试目录。
  • 已在 Android Studio 中编写过单元测试和插桩测试。
  • 已在 Android 项目中添加过 Gradle 依赖项。

学习内容

  • 如何为 ViewModelLiveData 编写单元测试?

所需条件

  • 一台安装了 Android Studio 的计算机。
  • Cupcake 应用的解决方案代码。

下载此 Codelab 的起始代码

在此 Codelab 中,您将基于之前的解决方案代码在 Cupcake 应用中添加插桩测试。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时,项目的 GitHub 页面会在浏览器中打开。
  2. 检查并确认分支名称与 Codelab 中指定的分支名称匹配。例如,在以下屏幕截图中,分支名称为 main

fe29aa9112862a93.png

  1. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个弹出式窗口。

5b0a76c50478a73f.png

  1. 在弹出式窗口中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open

a065e3d575fe607b.png

注意:如果 Android Studio 已经打开,则改为依次选择 File > Open 菜单选项。

4f3b1e628c7695f1.png

  1. 在文件浏览器中,转到解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 11c34fc5e516fb1c.png 以构建并运行应用。请确保该应用按预期构建。

2. 起始应用概览

Cupcake 应用包含一个主屏幕,主屏幕中显示一个订单屏幕,里面包含三个用于选择纸杯蛋糕数量的选项。点击某个选项后,您会转到另一个屏幕,您可在该屏幕中选择口味,然后再转到一个屏幕为订单选择取货日期。选择好之后,您可以将订单发送到另一个应用。在上述过程中,您可以在任何一个阶段取消订单。

3. 创建单元测试目录

按照您在之前的 Codelab 中执行的操作步骤,为 Cupcake 应用创建一个单元测试目录。

4. 创建单元测试类

创建一个名为 ViewModelTests.kt 的新类。

5. 添加必要的依赖项

将以下依赖项添加到项目中:

testImplementation 'junit:junit:4.+'
testImplementation 'androidx.arch.core:core-testing:2.1.0'

现在,同步项目。

6. 编写 ViewModel 测试

我们先从一个简单的测试开始:当我们在设备或模拟器上与应用互动时,首先要选择纸杯蛋糕的数量。因此,我们首先要测试 OrderViewModel 中的 setQuantity() 方法,并检查 quantity LiveData 对象的值。

我们要测试的 quantity 变量是 LiveData 的实例。测试 LiveData 对象需要执行一个额外的步骤,我们添加的依赖项就是要在此处发挥作用。我们使用 LiveData 在值更改后立即更新界面。界面运行在我们所谓的“主线程”上。如果您不熟悉线程和并发也没关系,我们会在其他 Codelab 中对这些内容进行深入介绍。眼下,就 Android 应用而言,您可以将主线程视为界面线程。用于向用户显示界面的代码在此线程上运行。除非另有指定,否则单元测试假定所有代码均在主线程上运行。不过,由于 LiveData 对象无法访问主线程,因此我们必须显式声明 LiveData 对象不应调用主线程。

  1. 为了指定 LiveData 对象不应调用主线程,我们需要在每次测试 LiveData 对象时提供一条特定的测试规则。
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
  1. 现在,我们可以创建一个名为 quantity_twelve_cupcakes() 的函数。在该方法中,创建 OrderViewModel. 的实例。
  2. 在此测试中,您将检查以确保在调用 setQuantity 时,OrderViewModel 中的 quantity 对象会更新。但在调用任何方法或使用 OrderViewModel 中的任何数据之前,请务必注意,在测试 LiveData 对象的值时,您需要观察各个对象以便发出更改。为此,一个简单的方法就是使用 observeForever 方法。对 quantity 对象调用 observeForever 方法。此方法需要 lambda 表达式,但该表达式可以留空。
  3. 然后,调用 setQuantity() 方法,并传入 12 作为参数。
val viewModel = OrderViewModel()
viewModel.quantity.observeForever {}
viewModel.setQuantity(12)
  1. 我们可以稳稳地推断出,quantity 对象的值为 12。请注意,LiveData 对象并非值本身。值包含在名为 value 的属性中。做出以下断言:
assertEquals(12, viewModel.quantity.value)

您的测试应如下所示:

@Test
fun quantity_twelve_cupcakes() {
   val viewModel = OrderViewModel()
   viewModel.quantity.observeForever {}
   viewModel.setQuantity(12)
   assertEquals(12, viewModel.quantity.value)
}

运行测试!恭喜,您刚刚编写了您的首个 LiveData 单元测试,这是 Modern Android Development 中的一项重要技能。此测试无法测试太多业务逻辑,所以,接下来我们要编写一个稍微复杂一些的测试。

OrderViewModel 的一项主要功能是计算订单的价格。当我们选择纸杯蛋糕的数量以及选择取货日期时,都会进行此项计算。价格计算是在一个私有方法中进行的,因此我们的测试无法直接调用该方法。只有 OrderViewModel 中的其他方法可以调用它。这些方法是公共方法,所以我们会调用这些方法来触发价格计算,以便检查价格的值是否符合预期。

最佳做法

选择纸杯蛋糕的数量和选择日期时,价格会更新。虽然这两种情况都应该进行测试,但一般而言,最好只测试一项功能。因此,我们要为每项测试编写单独的方法:一个函数用于测试数量更新时的价格,另一个不同的函数用于测试日期更新时的价格。我们绝不希望因为其他测试失败而导致测试结果失败。

  1. 创建一个名为 price_twelve_cupcakes() 的方法,并添加注解将其标记为测试。
  2. 在该方法中,创建一个 OrderViewModel 的实例并调用 setQuantity() 方法,传入 12 作为参数。
val viewModel = OrderViewModel()
viewModel.setQuantity(12)
  1. 查看 OrderViewModel 中的 PRICE_PER_CUPCAKE,可以看到纸杯蛋糕每个 2.00 美元。我们还可以看到,每次初始化 ViewModel 时都会调用 resetOrder(),并且在此方法中,默认日期是今天的日期,而 PRICE_FOR_SAME_DAY_PICKUP 为 3.00 美元。因此,12 * 2 + 3 = 27。我们的预期是,在选择 12 个纸杯蛋糕后,price 变量的值为 27.00 美元。所以,我们做出断言,预期值 27.00 美元等于 price LiveData 对象的值。
assertEquals("$27.00", viewModel.price.value)

现在,运行测试。

应该会失败!

17c8a24e4d7d635d.png

测试结果显示,实际值为 null。测试对此结果给出了解释。如果查看 OrderViewModel 中的 price 变量,您会看到以下内容:

val price: LiveData<String> = Transformations.map(_price) {
   // Format the price into the local currency and return this as LiveData<String>
   NumberFormat.getCurrencyInstance().format(it)
}

此示例旨在说明在测试中应观察到 LiveData 的原因。通过使用 Transformation 设置 price 的值。实际上,此代码会将分配给 price 的值转换为货币格式,这样我们就不必手动转换。不过,此代码还有其他作用。转换 LiveData 对象时,除非绝对必要,否则不会调用此代码,这样才能在移动设备上节省资源。只有在我们观察该对象是否发生了更改时,才会调用此代码。当然,这是在应用中完成的,但我们也需要在测试时执行相同的操作。

  1. 在测试方法中,请在设置数量前添加以下代码行:
viewModel.price.observeForever {}

您的测试应如下所示:

@Test
fun price_twelve_cupcakes() {
   val viewModel = OrderViewModel()
   viewModel.price.observeForever {}
   viewModel.setQuantity(12)
   assertEquals("$27.00", viewModel.price.value)
}

现在,如果您运行测试,测试应该会通过。

7. 解决方案代码

8. 恭喜

在此 Codelab 中,我们完成了以下内容:

  • 了解如何设置 LiveData 测试。
  • 了解如何测试 LiveData 本身。
  • 了解如何测试经过转换后的 LiveData
  • 了解如何在单元测试中观察 LiveData