阅读 631

【玩转Test】开篇-Android test 介绍

系列文章

前言

不会测试的开发不是好开发——鲁迅

一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,那么就从认识 test 开始吧

本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics

结构

作为 Android 开发者我们知道在 Android Studio 的 Android 视图中有三部分代码

  • app 的逻辑代码(main source set)
  • androidTest 代码
  • local test 代码

test 代码知道所有的 main source set 中的代码,因此可以测试这些类。但是 app 代码不知道 test 中的代码,并且 androidTest 和 test 都不知道对方的存在。事实上,当你构建出 apk 并提交应用市场时,测试代码并没有包含在内

依赖引用

下面标记的依赖 使用了 test 的引用方式 testImplementationandroidTestImplementation

注意:这些 test 代码不会打包到最终的 apk 文件中

testImplementation 引用的 JUnit 依赖只能在 test source set 中使用,这种依赖范围的限制是 Gradle 实现的

简单总结下:

  • 三种 source setsmaintestandroidTest
  • 测试代码能访问 app 代码
  • app 代码 不能 访问测试代码
  • 测试不会被打入到 Apk 中
  • 依赖范围包括:testImplementationandroidTestImplementation

运行第一个 test

我们打开 test source set ,看到其中有一个 ExampleUnitTest

ExampleUnitTest
ExampleUnitTest

可以看到其内部只有一个 addition_isCorrect() 方法

有两个要素使它成为一个 test:

  • 使用了 @Test 注解
  • 它存在于两个 test source set 之一

有了这两个要素,这个方法就可以独立的作为 test 运行

本示例 test 测试的内容在第 15 行,它被称之为断言(assertion

断言是 test 的核心内容,它检查你的代码或者 app 行为是否符合你的预期

本示例中,断言检查 4 是否等于 2 + 2

按照规定,您需要将你的 预期结果 传入到 expected 参数中,将 实际结果 传入到 actual 参数中

@Test 注解和断言语句都是 JUnit 下的

关于 JUnit 的更详细的的信息,请移步 官方文档

让我们开始运行一下这个 test,右击该方法,点击 Run

紧接着,Run 窗口会出现

可以看到该窗口显示了 test 的信息,显示出 test 是否通过以及有多少 test 通过

下面我们尝试一个 test 不通过的情况,我们加入一个断言,如下所示

这次我们点击 Run 窗口的绿色按钮来运行 test

我们可以看到,即使只有一个断言失败,整个 test 失败了

窗口指出了预期的结果为 5,而实际的结果为 4,并且下边标记处错误发生在第 15 行,可以看到这的确是个 bug

解决好 bug ,我们再次运行 test 。这次我们使用一个不一样的方式

下面介绍一些其他运行 test 的方式

可以右击类名选择 Run 选项

也可以在左侧视图中右击 test source set 选择 Run 按钮,该方法会运行所有 test。在顶部可以切换要运行的 test ,点击绿色按钮可以运行。也可切换回 app

androidTest VS test

下面我们来对比一下 androidTesttest

test androidTest
Local Tests Instrumented Tests
Local machine JVM Real or emulated devices
Faster Slower

我们来运行一个 androidTest,可以看到启动了模拟器

写一个 test

首先我们针对一个功能来创建 test,如图所示,存在一个 getForkAndOriginRepoStats() 方法用于获取 fork 仓库和原始仓库的数据并返回 StatsResult,其中 StatsResult 第一个参数为 fork 项目的百分比,第二个参数为原始项目的百分比。我们调用 Generate ,选中 test 选项,在弹出框中选择 JUnit4,点击 OK 并选择存放在 local test 中。这样我们就创建了一个 test

接下来我们编写 test 。可以看到自动创建出的 test 路径与 app code 中的代码路径的包名是对应的。我们先测试项目列表只有一个 item,并且没有 fork ,然后计算 fork 项目的百分比和原始项目的百分比。理论上讲,fork 项目的百分比为 0 ,而原始项目的百分比为 100%,代码如下图所示,我们编写完毕后点击运行

可以看到测试通过

这是一个正常流程,我们还需要测试异常的流程,比如 repos 为 empty list 或者 repos 变量本身为 null

可以看到我们的代码中没有针对 list 为 empty 或 null 做判断,所以导致了空指针,之后我们修改代码后即可通过测试

事实上,我们上面的编码流程叫做 Test Driven Development(TDD) 有关 TDD 的更多信息,可以移步 Test-Driven Development on Android with the Android Testing Support Library (Google I/O '17)

让你的 test 更具可读性

与写普通代码一样,您需要让您的 test 代码更具可读性,可以从三个方向入手

  • 优秀的命名
  • Given/When/Then
  • 借助断言库

优秀的命名

首先我们来谈谈命名,我们知道 test 方法使用 @Test 注解标记,理论上方法名可以随意命名,但随意的命名会导致可读性的降低,因此需要一些特定的命名规范

测试模块_ 动作或输入_ 结果状态

例如上面的例子我们的命名为:getForkAndOriginRepoStats_noForked_returnHundredZero

第一部分显示我们要测试的是 getForkAndOriginRepoStats() 方法,第二部分代表我们需要的是没有 fork 仓库的数据源,第三部分是结果的状态,0%

Given/When/Then

说完了命名我们来谈谈 Given/When/Then

测试的基本结构是 Given X,When Y,Then Z

还是上面的例子

  • Given 为你的测试逻辑提供数据源
  • When 是你的实际操作
  • Then 检查 test 是否通过

借助断言库

上面示例最后的断言代码让人看着很别扭,我们可以借助断言库来提高这部分的可读性

// 之前
assertEquals(result.forkPercent, 0f)

// 之后
assertThat(result.forkPercent, `is`(0f))
复制代码

下面的语句就像人类的一句话,翻译下来就是 断言 forkPercent 是 0f

这样的写法需要引入一个库 Hamcrest

testImplementation "org.hamcrest:hamcrest-all:1.3"
复制代码

注意:由于 is 是 kotlin 中的关键字,因此使用 `is` 来转义

常用的断言库

测试范围

测试范围指一个 test 测试多少代码

例如自动化测试根据测试范围可以分为

  • Unit Tests(单元测试)
  • Integration Tests(集成测试)
  • End to end Tests(端到端测试)

您的测试策略需要覆盖到所有的类型

Unit Tests

上面的示例我们已经写过了 Unit Tests

  • 范围是单个方法或类
  • 帮助查明失败原因
  • 应该运行的很快,通常是本地测试
  • 低保真度

他们的范围是单个的方法或类

如果 Unit Tests 失败了,您知道您的代码在哪里出了问题。因为它聚焦于很小一段代码

Unit Tests 也意味着可以快速运行,由于您频繁地修改代码会使得它会频繁的运行,因此需要速度。Unit Tests 通常是本地测试

它们有较低的保真度,因为现实世界您的 app 要执行很多代码而不仅仅是一个方法或者类

Unit Tests 就像检查一个链条的每个环节是否能够正常运行

但它不检查这些环节组合在一起是否能够运行,为此您需要 Integration Tests

Integration Tests

Integration Tests 拥有更大的范围

  • 范围是几个类或单个功能
  • 确保几个类共同运行
  • 可以使用本地测试或机器测试

就像 Integration 这个词一样,Integration Tests 整合一些类确保他们组合起来的表现符合预期

构建 Integration Tests 的方式是让他们测试单个功能,就像获取指定用户的 Github 仓库

Unit Tests 相比,Integration Tests 有着更大的范围,但他们仍运行的很快并且有着很好的保真度

根据具体情况来判断使用本地测试还是机器测试,例如如果您写的 Integration Tests 涉及到了 UI 组件,那么您需要使用真机来测试了

End to end Tests

第三种类型是 End to end Tests,该测试将一些列功能组合起来一起运行

  • 范围是 app 的大部分
  • 高保真度
  • 将 app 作为整体来测试
  • 接近真实地使用,应该使用设备测试

End to end Tests 测试 app 的大部分,它十分接近真实地使用,因此速度上会比较慢

它有着最高的保真度并确保您的应用作为一个整体运行

这些测试应该使用设备测试

测试比重

推荐的测试比例是 70% 的单元测试,20% 的集成测试,以及10% 的端到端测试

您能否轻松地在各个部分测试您的 app 取决于您的 app 使用的结构

例如,您的应用将所有逻辑都放置在一个 activity 的大的方法中,您可能可以写出端到端测试,但单元测试和集成测试则写不出来

一个更好的架构应该将应用的逻辑拆分为多个方法和类,这允许每部分可以独立的测试

对于单元测试,您可以测试 ViewModelRepository 以及 DAO

对于集成测试,您可以组合测试 fragmentViewModel ,或者您可以测试整个数据库代码

端到端测试会测试整个应用

关于测试的原理,可移步 官方文档

test 的 codelab

关于我

我是 Fly_with24