返回 登录
0

快速提升 Android App 的代码覆盖率

阅读2551

原文:Improving Code Coverage In Android App
作者:Sergey Grekov
翻译:ASCE1885
审校:屠敏,关注移动开发领域,寻求报道或投稿请发邮件tumin@csdn.net

编写移动应用程序很难,编写好的和可维护的应用程序就更加困难了。在开发过程中,我们需要保证对代码库的每次修改不会降低代码质量和功能的可用性。

在现代的移动应用开发中,很难想象在没有编写测试特别是单元测试的情况下,你可以做出一个可靠的和可维护的应用。但我们经常遇到一个问题:要编写多少的测试用例才能足够保证这段代码能够被测试正确的覆盖。嗯,这没有明确的答案,但今天我想介绍一个名为 JaCoCo 这款很棒的工具,它有助于保证有价值的代码被单元测试覆盖到。

JaCoCo 全称是 Java Code Coverage tool,它已经在 Java 开发者中使用数十年了,但如果配置得当的话, Android 开发者也可以利用它获得益处。社区已经有很多文章介绍 Android 工程中如何配置 JaCoCo 从而生成测试覆盖报告,因此我就不再深入介绍这个主题。相反我将展示如何为 JaCoCo 测试覆盖配置自动校验功能,从而方便将其引入你的构建或者 CI 管道(pipeline)中。

首先让我们看一个很简单的 App,它包含两个页面:登录页面和主页面。

请注意,这个示例工程只作演示用途,并不具备作为生产版本发布和使用的能力。

fusion.jpg

你可以在 Github 上找到这个工程的完整代码。

在这个示例工程中我实现了一个很基础的 MVP 模式。这意味着我们将 Activities 视为被动的 View,所有有用的 UI 逻辑放在 Presenters 中。让我们仔细看下 LoginPresenter,它有一个键盘输入处理,登录和密码的校验,以及授权逻辑本身。如果一切校验没有问题的话,那么登录成功后我们就进入 MainActivity。在 LoginPresenterTest 中有一些单元测试。MVP 模式的规则是保持 View 层拥有尽可能少的代码,同时,将业务逻辑保留在 Presenters 中,从而使得这部分逻辑代码能够很容易被单元测试覆盖到。我们将按这种方式来配置 JaCoCo。我们将忽略 View 层和一些 Android 相关的类,为除此以外其他的类的代码生成测试覆盖报告。

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.7.201606060606"
}

def fileFilter = [
        'com/androidjacoco/sample/**/view/**.*',
        '**/R.class', '**/R$*.class', '**/BuildConfig.*',
        '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter)
def mainSrc = "${project.projectDir}/src/main/java"

task customJacocoTestReport(type: JacocoReport, dependsOn: 'test') {

    reports {
        html.enabled = true
        html.destination = "${buildDir}/reports/jacoco"
    }

    sourceDirectories = files([mainSrc])
    classDirectories = files([debugTree])
    executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec")
}

如果我们的工程结构是按照特性来组织的,那么通过配置 'com/androidjacoco/sample/**/view/**.*' 这个过滤器规则就可以忽略现在以及将来可能增加的所有的 View 类。

现在,如果我们在命令行中执行:

./gradlew customJacocoTestReport

我们将得到类似下面这样一份报告:

1-H19gQ1keuq9-zf0ZwTG6hQ.png

这里我们可以看到报告中展示了一些数据类和 Presenters 的覆盖率。

我们对下面三列感兴趣:

  • Missed Instructions:提供关于被执行(译者注:指被单元测试覆盖到)或者没有被执行的代码量信息,单位是一条 Java 字节码指令。
  • Missed Branches:用于计算一个方法中此类分支的总个数,并确定被执行或者没有被执行的分支数量。
  • Classes:一个类只有当其中至少一个方法被执行过,才认为这个类是已执行的。

关于报告的更详细信息可以参见这里

让我们看一下 LoginPresenter 类,并确保其中确实有很多没有被执行到的指令和分支。

1-IIMgFlicCOEAdUU2_RZVsw.png

这里也要注意一下,匿名类,例如上面代码中的 SingleObserver 或者 Consumer 类,在报告中会当作独立的类来计算,正如我们在上面报告的 com.androidjacoco.sample.login.presenter 这个包所对应的 classes 列的值可以看到的。

现在如果我们不想让测试覆盖率如此低的代码进入到生产环境中,我们能做些什么呢?JacocoCoverageVerification 这个 task 能够帮我们解决这个问题!

task customJacocoTestCoverageVerification(type: JacocoCoverageVerification, dependsOn: 'JacocoTestReport') {

    sourceDirectories = files([mainSrc])
    classDirectories = files([debugTree])
    executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec")


    violationRules {
        setFailOnViolation(true)

        rule {
            element = 'PACKAGE'

            limit {
                value = 'COVEREDRATIO'
                counter = 'BRANCH'
                minimum = 0.8
            }
        }

        rule {
            element = 'PACKAGE'

            limit {
                value = 'COVEREDRATIO'
                counter = 'INSTRUCTION'
                minimum = 0.6
            }
        }

        rule {
            element = 'PACKAGE'
            includes = ['com.androidjacoco.sample.*.presenter']
            enabled = true

            limit {
                counter = 'CLASS'
                value = 'MISSEDCOUNT'
                maximum = 0
            }
        }
    }
}

这个 task 依赖于前面的 customJacocoTestReport task,如果执行它,将会首先生成测试覆盖报告然后接着对其进行分析。这个 task 的配置中包含如何找到源代码类所在的路径,以及 violationRules 配置块。violationRules 配置块本身又包含了一些规则块,只要你愿意,你可以添加尽可能多的这种规则块。在这个例子中,我配置了三个规则块:

规则一

如果没有被单元测试覆盖到的指令比率高于 60%,那么构建将会失败:

rule {
    element = 'PACKAGE'
    limit {
        value = 'COVEREDRATIO'
        counter = 'INSTRUCTION'
        minimum = 0.6
    }
}

规则二

如果代码分支(if,switches 等)有多达 80% 以上没有被单元测试覆盖到,那么构建将会失败:

rule {
    element = 'PACKAGE'
    limit {
        value = 'COVEREDRATIO'
        counter = 'BRANCH'
        minimum = 0.8
    }
}

规则三

在这个规则中我声明了在 presenter 包中的每个类都必须至少有一个单元测试。请注意所有匿名类都会被当作一个个独立的类来对待。

rule {
    element = 'PACKAGE'
    includes = ['com.androidjacoco.sample.*.presenter']
    enabled = true
    limit {
        counter = 'CLASS'
        value = 'MISSEDCOUNT'
        maximum = 0
    }
}

想要了解更多关于规则的语法,可查看:http://www.jacoco.org/jacoco/trunk/doc/ant.html。(译者注:关于在 gradle 中配置 JaCoCo,可以参见这里

此时我们执行命令:

./gradlew customJacocoTestCoverageVerification

将会得到如下所示的由于违反规则所导致的错误:

FAILURE: Build failed with an exception.* What went wrong:
Execution failed for task ‘:app:customJacocoTestCoverageVerification’.
> Rule violated for package com.androidjacoco.sample.login.presenter: branches covered ratio is 0.3, but expected minimum is 0.5
 Rule violated for package com.androidjacoco.sample.login.presenter: instructions covered ratio is 0.1, but expected minimum is 0.8
 Rule violated for package com.androidjacoco.sample.login.presenter: classes missed count is 3, but expected maximum is 0

从上面可以看到,示例工程的覆盖率不符合我们规则,因此需要编写更多的单元测试!

在这个示例工程仓库的 VerificationPassed 分支,你可以看到覆盖率满足规则的代码。通过这种方式,你可以配置构建脚本,并将测试覆盖率验证添加到构建过程中。

JaCoCo 是一个功能强大的工具,可以帮助你增加代码仓库的测试覆盖率。当然,如何定义规则和代码覆盖率取决于你的和具体项目的需求,但这是一个很好的代码仓库的健康指标,值得尝试将其引入你的构建管道中。在我们的项目中,测试覆盖率从开始的 0.4 慢慢增加到 0.6,并且还在继续增长。

参考:

评论