Android 单元测试基础

语言: CN / TW / HK

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

本文参考自:https://developer.android.com/training/testing/fundamentals?hl=zh-cn#testing-pyramid

在大多数公司的实际的 Android 开发中,大多数都是使用黑盒测试,直接运行项目从用户操作流程来进行测试工作的。本文主要介绍单元测试在 Android 中的应用和一些常见的框架。

Android 单元测试基础

测试是应用程序开发过程中不可或缺的一部分。通过对应用程序持续运行测试,您可以在发布应用程序之前验证其正确性、功能行为和可用性。 移动应用程序很复杂,必须在许多环境中正常工作。因此,有许多类型的测试。

例如,根据测试目标的不同,可以划分为不同的类型: - 功能测试:App 做了它应该做的事情了吗? - 性能测试:能否快速有效的完成工作? - 无障碍测试:是否适用于无障碍服务? - 兼容性测试:App 是否适用于所有设备和 API 级别?

测试也可根据测试范围的大小和隔离程度划分为不同的级别: - 单元测试或小型测试仅验证应用程序的一小部分内容,例如方法或类。 - 端到端测试或大型测试同时验证应用程度的较大的模块,例如整个屏幕或用户输入流。 - 中型测试介于两者之间,并检查两个或多个模块之间的集成。

image.png

有很多方法可以对测试进行分类。但是,对于应用程序开发人员来说,最重要的区别是测试运行的位置。

测试金字塔(如下图所示)说明了应用应如何包含三类测试(即小型、中型和大型测试):

pyramid.png

  • 小型测试是指单元测试,用于验证应用的行为,一次验证一个类或一个方法。
  • 中型测试是指集成测试,用于验证模块之间的互动。
  • 大型测试是指 End-to-End 测试,用于验证跨越了应用的多个模块的用户操作流程。

沿着金字塔逐级向上,从小型测试到大型测试,各类测试的保真度逐级提高,但维护和调试工作所需的执行时间和工作量也逐级增加。因此,您编写的单元测试应多于集成测试,集成测试应多于端到端测试。虽然各类测试的比例可能会因应用的用例不同而异,但我们通常建议各类测试所占比例如下:小型测试占 70%,中型测试占 20%,大型测试占 10%

如需详细了解 Android 测试金字塔,请观看 2017 年 Google I/O 大会的 Android 平台上的测试驱动型开发会议视频(从 1 分 51 秒开始)。

小型测试

小型测试应该是高度集中的单元测试,能够详尽地验证应用中每个类的功能和职责。测试 Android 应用程序,可以通过在一台 Android 设备或计算机上进行,而根据运行环境可以分为两种: - 插桩测试:在 Android 设备上运行,可以是模拟器也可以是物理设备。该应用程序通过注入命令与读取状态的测试应用程序一起构建和安装。通常用于 UI 测试,会启动应用程序然后与之进行交互。 - 本地测试:本地测试会在开发机或者服务器上执行,也称之为 host-side tests 。这种测试执行速度更快,并且会将测试对象与应用程序的其他部分隔离开。

image.png

并非所有单元测试都是本地的,也并非所有端到端测试都在设备上运行。例如:

  • 大型本地测试:可以使用在本地运行的安卓模拟器,比如Robolectric
  • 小型插桩测试:您可以验证您的代码是否适用于框架功能,例如 SQLite 数据库。您可以在多个设备上运行此测试以检查与多个 SQLite 版本的集成。

例如,以下代码片段演示了如何在 检测的 UI 测试中与 UI 交互,该测试单击一个元素并验证另一个元素是否显示。

```kotlin // When the Continue button is clicked onView(withText("Continue"))     .perform(click())

// Then the Welcome screen is displayed onView(withText("Welcome"))     .check(matches(isDisplayed())) ```

此代码段显示了 ViewModel 单元测试的一部分(本地测试): ```kotlin val viewModel = MyViewModel(myFakeDataRepository)

viewModel.loadData()

// Then it should be exposing data assertTrue(viewModel.data != null) ```

本地单元测试

本地单元测试是直接在自己的开发计算机上直接运行测试代码,无需依赖 Android 相关的环境。本质上是使用本地 JVM 而不是 Android 设备运行代码。

本地测试可以更快的评估程序的逻辑,但是无法与 Android 框架进行交互会很大程度限制可测试的内容。一个典型的例子是,当我们需要 Android App 的 Context 时,就很难直接获取到。(使用第三方框架即可,后续会说明)。

默认情况下,本地单元测试的源文件放在 module-name/src/test/. 当您使用 Android Studio 创建新项目时,此目录已经存在。

为项目配置测试依赖,单元测试主要是使用 Junit 测试框架提供的标准 API。 groovy dependencies {   // Required -- JUnit 4 framework   testImplementation "junit:junit:$jUnitVersion"   // Optional -- Robolectric environment   testImplementation "androidx.test:core:$androidXTestVersion"   // Optional -- Mockito framework   testImplementation "org.mockito:mockito-core:$mockitoVersion"   // Optional -- mockito-kotlin   testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"   // Optional -- Mockk framework   testImplementation "io.mockk:mockk:$mockkVersion" } 这里用到了几个三方框架: - Robolectric :用于模拟 Android 环境。 - Mockito :一个小型库,提供辅助函数以在 Kotlin 中使用 Mockito。 - Mockk :kotlin 的 mock 框架。

一个简单的本地测试:

kotlin class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } 通过断言方法 assertTrue 可以检查返回的结果是否符合预期。当前直接运行是会运行成功的,展示效果如下:

image-20220607153910441.png

而如果想构建一个失败的单元测试,可以将方法中的 assertEquals 修改为: kotlin assertEquals(4, 2 + 1) 这样运行失败后,IDE 会有错误日志:

image-20220607154241733.png

在文章的前面有一个问题是本地单元测试不依赖 Android 环境,无法访问 Android 框架中的内容,比如通过 Context 去获取字符串资源 : kotlin context.resources.getString(...) 使用 Mockable Android 库和 MockitoMockK等模拟框架,您可以在单元测试中对 Android 类的模拟行为进行编程。 执行以下步骤: 1. 添加 Mockito 和 MockK 的依赖 2. 在测试类上添加注解 @RunWith(MockitoJUnitRunner.class) 。此注释告诉 Mockito 测试运行程序验证您对框架的使用是否正确并简化了模拟对象的初始化。 3. 要为 Android 依赖项创建模拟对象,请@Mock在字段声明之前添加注释。 4. 编写测试代码

```kotlin @RunWith(MockitoJUnitRunner::class) class MockedContextTest {

@Mock   private lateinit var mockContext: Context

@Test   fun readStringFromContext_LocalizedString() {     val mockContext = mock {         on { getString(R.string.name_label) } doReturn "FAKE_STRING"     }     val myObjectUnderTest = ClassUnderTest(mockContext)     val result: String = myObjectUnderTest.getName()     assertEquals(result, FAKE_STRING)   } } ``` 也可以通过 Robolectric 的 API 来获取一个 Activity 作为 Context 对象,注意它们的注解中的测试运行程序不同:

```java @RunWith(RobolectricTestRunner.class) public class MyActivityTest { @Test public void clickingButton_shouldChangeMessage() { MyActivity activity = Robolectric.setupActivity(MyActivity.class);

String text = activity.resources.getString(R.string.app_name);

assertThat(text).isEqualTo("Robolectric");

} } ```

插桩单元测试

插桩单元测试运行在 Android 设备上,无论是模拟器还是真实物理设备都可以。它们会反映更多实际运行会发生的情况,但速度会慢很多。

默认情况下,插桩单元测试的源文件放在 module-name/src/androidTest/. 当您使用 Android Studio 创建新项目时,此目录已经存在。

官方建议仅在必须针对真实设备的行为进行测试的情况才使用这种单元测试。

在开始之前,您应该添加 AndroidX 测试 API,它允许您为您的应用程序快速构建和运行经过检测的测试代码。AndroidX Test 包括一个 JUnit 4 测试AndroidJUnitRunner运行程序,以及用于功能 UI 测试的 API,例如EspressoUI AutomatorCompose 测试

设置依赖内容: groovy dependencies {     androidTestImplementation "androidx.test:runner:$androidXTestVersion"     androidTestImplementation "androidx.test:rules:$androidXTestVersion"     // Optional -- UI testing with Espresso     androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"     // Optional -- UI testing with UI Automator     androidTestImplementation "androidx.test.uiautomator:uiautomator:$uiAutomatorVersion"     // Optional -- UI testing with Compose     androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" } 插桩测试类的测试运行器应该是一个 Junit4 类,需要在测试类上添加注解: kotlin @RunWith(AndroidJUnit4::class) class LogHistoryAndroidUnitTest 在插桩测试中,如果需要访问 Android 框架中的内容,也可通过上述第三方框架 Robolectric 、Mockito 等实现,也可直接使用 androidx.test 提供的 API 访问 Android 框架中的内容,例如: kotlin @RunWith(AndroidJUnit4::class) class TasksViewModelTest { @Test fun addNewTask_setsNewTaskEvent() { val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) tasksViewModel.addNewTask() } }

其他资源

通过命令行执行插桩测试:https://developer.android.com/studio/test/command-line