不会测试的开发不是好开发——鲁迅
一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,那么就从认识 test 开始吧
本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics
作为 Android 开发者我们知道在 Android Studio 的 Android 视图中有三部分代码
test 代码知道所有的 main source set 中的代码,因此可以测试这些类。但是 app 代码不知道 test 中的代码,并且 androidTest 和 test 都不知道对方的存在。事实上,当你构建出 apk 并提交应用市场时,测试代码并没有包含在内
下面标记的依赖 使用了 test 的引用方式 testImplementation
和 androidTestImplementation
注意:这些 test 代码不会打包到最终的 apk 文件中
testImplementation
引用的 JUnit
依赖只能在 test source set 中使用,这种依赖范围的限制是 Gradle
实现的
简单总结下:
main
,test
,androidTest
testImplementation
和 androidTestImplementation
我们打开 test source set ,看到其中有一个 ExampleUnitTest
可以看到其内部只有一个 addition_isCorrect()
方法
有两个要素使它成为一个 test:
@Test
注解有了这两个要素,这个方法就可以独立的作为 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
和 test
test | androidTest |
---|---|
Local Tests | Instrumented Tests |
Local machine JVM | Real or emulated devices |
Faster | Slower |
我们来运行一个 androidTest
,可以看到启动了模拟器
首先我们针对一个功能来创建 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 方法使用 @Test 注解标记,理论上方法名可以随意命名,但随意的命名会导致可读性的降低,因此需要一些特定的命名规范
测试模块_ 动作或输入_ 结果状态
例如上面的例子我们的命名为:getForkAndOriginRepoStats_noForked_returnHundredZero
第一部分显示我们要测试的是 getForkAndOriginRepoStats()
方法,第二部分代表我们需要的是没有 fork 仓库的数据源,第三部分是结果的状态,0%
说完了命名我们来谈谈 Given/When/Then
测试的基本结构是 Given X,When Y,Then Z
还是上面的例子
上面示例最后的断言代码让人看着很别扭,我们可以借助断言库来提高这部分的可读性
// 之前 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
他们的范围是单个的方法或类
如果 Unit Tests
失败了,您知道您的代码在哪里出了问题。因为它聚焦于很小一段代码
Unit Tests
也意味着可以快速运行,由于您频繁地修改代码会使得它会频繁的运行,因此需要速度。Unit Tests
通常是本地测试
它们有较低的保真度,因为现实世界您的 app 要执行很多代码而不仅仅是一个方法或者类
Unit 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 的大部分,它十分接近真实地使用,因此速度上会比较慢
它有着最高的保真度并确保您的应用作为一个整体运行
这些测试应该使用设备测试
推荐的测试比例是 70% 的单元测试,20% 的组装测试,以及10% 的端到端测试
您能否轻松地在各个部分测试您的 app 取决于您的 app 使用的结构
例如,您的应用将所有逻辑都放置在一个 activity 的大的方法中,您可能可以写出端到端测试,但单元测试和组装测试则写不出来
一个更好的架构应该将应用的逻辑拆分为多个方法和类,这允许每部分可以独立的测试
对于单元测试,您可以测试 ViewModel
,Repository
以及 DAO
对于组装测试,您可以组合测试 fragment
和 ViewModel
,或者您可以测试整个数据库代码
端到端测试会测试整个应用
关于测试的原理,可移步 官方文档
test 的 codelab
我是 Fly_with24