Android Gradle最佳实践系列6:运行测试

1,768 阅读11分钟

为了确保项目App或Library的质量,自动化测试非常重要。一直以来,Android开发工具缺乏对自动化测试的支持,但随着Android数年的发展,Google已经付出了很多努力,使开发人员更容易测试Android代码。一些旧框架已更新,并添加了新框架,以确保我们可以彻底测试App和Library。我们不仅可以从Android Studio运行它们,还可以直接从命令行界面使用Gradle运行测试用例。

在本章中,我们将探讨测试Android应用和库的不同测试方法,还将了解Gradle如何构建自动化测试过程。

本章主要包括如下内容:

  • 单元测试
  • 功能(UI)测试
  • 测试覆盖率

单元测试

在项目中编写良好的单元测试不仅可以确保程序的质量,而且还可以方便地检查新代码是否破坏了已有功能代码的正确性。 Android Studio和Android Gradle Plugin原生支持单元测试,但还是需要我们做适当配。

JUnit

JUnit是一个非常受欢迎的单元测试库,已经存在了十多年。通过它编写测试代码非常容易并且测试代码也易于理解和阅读。请记住,这些特定的单元测试仅用于测试业务逻辑代码,与Android SDK相关的代码是不能通过Junit做测试的,如果我们想在单元测试代码中测试Android SDK相关代码,可以使用Roboletric框架来实现,接下来章节会讲到。

在我们开始为Android项目编写JUnit测试之前,需要为测试创建一个目录。按照惯例,这个目录称为 test ,它应该在与主目录相同的级别。目录结构如下所示:

app
 |-src
 |   |-main
 |   |    |-java
 |   |        |-com.example.app
 |   |-res       
 |-test
     |-java
        |-com.example.app

我们可以在src/test/java/com.example.app创建测试用例

我们需要添加JUnit库依赖后,才能使用相关功能,我们可以使用JUnit 4.x版本,通过添加测试构建的依赖来确保这一点:

dependencies {
    testImplementation 'junit:junit:4.12'
}

注意,这里使用的是 testImplementation 而不是 implementation 。此配置用来确保Junit依赖仅在运行测试时才会被编译(在非测试代码中也无法引用JUnit库代码),而不是在打包应用程序时构建。 使用 testImplementation 添加的依赖不会包含在由正常打包生成的APK中。

如果我们在某个Build Type或Product Flavor中有任何特殊条件,可以单独地向该特定类型添加仅测试依赖。例如,如果只想将JUnit测试添加到付费版中,则可以执行以下操作:

dependencies {
    testPaidImplementation 'junit:junit:4.12'
}

当配置完善,就可以开始写测试用例。下面是一个简单的例子,测试两个数字相加的方法:

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class LogicTest {
       @Test
       public void addingNegativeNumberShouldSubtract() {
           Logic logic = new Logic();
           assertEquals("6 + -2 must be 4", 4, logic.add(6, -2));
           assertEquals("2 + -5 must be -3", -3, logic.add(2, -5));
       }
}

要使用Gradle运行所有测试,只需执行 gradlew test 命令。如果只想对某个Build Variant运行测试,只需添加对应Build Variant的名称。例如:我们仅仅想对debug variant运行测试,只需执行 gradlew testDebug 。 如果测试失败,Gradle会在命令行界面中显示错误消息。 如果所有测试运行顺利,Gradle则显示BUILD SUCCESSFUL消息。

单个失败的测试将导致测试任务失败,会立即停止整个过程。 这意味着在失败的情况下有的测试用例就根本没来得及执行。如果要确保整个Test suite对所有构建版本都执行,可以使用continue标志:

$ gradlew test --continue

为不同的Build Variant编写测试,需要在指定的Build Variant代码目录下添加测试代码。例如,如果想测试应用程序的付费版本中的特定行为,就需要将测试类放在src/testPaid/java/com.example.app中。

如果不想运行整个Test Suit,而是只运行一个特定类的测试,你可以这样使用:

$ gradlew testDebug --tests="*.LogicTest"

执行测试任务不仅运行所有测试,最后还会创建一个测试报告,可以在 app/build/reports/tests/debug/index.html 找到。 如果有任何失败,此报告可以很容易找到测试出现的问题。在自动执行测试的情况下测试报告会特别有用。默认情况下,Gradle会为运行测试的每个Build Variant创建一个测试报告。

如果所有测试成功运行,我们的单元测试报告展示效果如下:

在Android Studio中也可运行测试。在AS中运行单元测试,会立即在IDE得到反馈,如果测试失败,我们可以点击失败的测试并导航到相应的代码。如果所有测试通过, Run 窗口将如下所示:

如果测试代码中要包含对特定Android的类或资源部分做测试,常规单元测试并不理想。我们尝试运行,但是我们会得到如下 java.lang.RuntimeException:Stub! 错误 。 要解决这个问题,我们需要自己实现Android SDK中的每个方法,或者使用Mock框架。幸运的是,有几个第三方库已经处理了Android SDK。 最流行的当是Robolectric,它提供了一种简单的方法来测试Android代码的功能,而不需要一个设备或模拟器。

Robolectric

使用Robolectric做单元测试,可以编写使用Android SDK和Resource的测试用例。这些测试用例可以直接在Java虚拟机中运行测试,这意味着我们不需要使用真机或模拟器来运行测试用例,因此可以更快地测试应用程序或库的UI组件的行为。

要开始使用Robolectric,我们需要在build.gradlew文件中设置 includeAndroidResources 为true ,这个设置是为了让单元测试可以访问到android相关资源文件,如assets、manifest、图片、字符资源等等你;然后就是在依赖配置中加入对Robolectric框架的配置

android {
  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
}

dependencies {
//
  testImplementation 'org.robolectric:robolectric:4.6.2'
}

Robolectric测试类就像常规单元测试一样,应该放在 src/test/java/com.example.app 目录中。 不同之处在于,我们现在可以编写涉及Android类和资源的测试。例如,下面的测试代码用来验证某个TextView的文本在单击特定按钮后是否发生更改:

MainActivity逻辑比较简单,界面中点击btn,textView文字变为“Hello World!”

后续编码采用kotlin,请注意

class MainActivity : AppCompatActivity() {
    lateinit var button: Button
    lateinit var message: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button = findViewById(R.id.btn_click)
        button.setOnClickListener { message.text = "Hello World!" }
        message = findViewById(R.id.textView)
    }
}

我们通过Robolectric测试MainActivity如下代码所示,这里需要注意ActivityScenario为androidx test core框架相关api,这是官方对Robolectric等三方框架做的测试适配,在mock android相关组件(Activity,Service,Intent,Context等)都应该通过test core框架中的api获取。

testImplementation 'androidx.test:core:1.4.0'

添加test core依赖,然后运行如下代码

@RunWith(RobolectricTestRunner::class)
class MainActivityTest {

    @Test
    fun clickingButton_shouldChangeMessage() {
        val activityScenario = ActivityScenario.launch(MainActivity::class.java)
        activityScenario.onActivity { activity ->
            activity.button.performClick()
            assertEquals(activity.message.text, "Hello World!")
        }

    }
}

功能(UI)测试

功能测试(Functional tests) 用于测试应用程序的几个UI组件是否按预期正常工作。例如,我们可以创建一个功能测试,以确认点击某个按钮会打开一个新的Activity。当前开源社区上有一些Android的第三方功能测试框架,但是最简单的方法还是使用官方建议的Espresso框架。

Espresso

Google创建的Espresso库,是的开发人员更轻松地编写功能测试代码。我们可以通过AndroidX Library依赖Espresso模块。

为了在设备上运行测试,你需要定义Test Runner。通过测试的Support Library,Google提供了AndroidJUnitRunner Test Runner,它可以帮助你在Android设备上运行JUnit测试类。测试运行程序会将应用程序APK和测试APK加载到设备中,并运行所有测试,然后生成测试报告。

如果您已经下载了Espresso Support Library,可以通过如下方式引入Espresso:

android {
    defaultConfig {
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

你还需要设置一些依赖项,然后才能开始使用Espresso:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test:rules:1.1.0'   
}

您需要引用测试的androidx test和espresson库来开始使用Espresso。

请注意,这些依赖关系使用androidTestCompile配置,而不是我们之前使用的testCompile配置。这是为了区分单元测试和功能测试。

以上各个设置完成后,我们就可以开始添加测试。功能测试放置在与常规单元测试不同的目录中。就像使用依赖配置一样,你需要使用androidTest而不是test,所以正确的目录是src/androidTest/java/com.example.app下面是一个测试类的示例,它检查MainActivity中的TextView的文本是否正确:

@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun listGoesOverTheFold() {
        onView(withText("Hello!")).check(matches(isDisplayed()))
    }
}

在运行Espresso测试之前,我们需要确保真机或模拟器连接正常。如果忘记连接设备,测试任务将抛出如下异常:

Execution failed for task ':app:connectedAndroidTest'.
>com.android.builder.testing.api.DeviceException:
java.lang.RuntimeException: No connected devices!

连接真机或启动模拟器后,可以通过 gradlew connectedCheck 命令运行Espresso测试。 此任务将执行 connectedAndroidTest 任务以运行所有连接的设备上的Debug构建类型的测试,并使用 createDebugCoverageReport 创建测试报告。

如果运行espresson遇到如下这个错误:

Error: duplicate files during packaging of APK app-androidTest.apk
     Path in archive: LICENSE.txt
     Origin 1: ...\hamcrest-library-1.1.jar
     Origin 2: ...\junit-dep-4.10.jar

错误信息本身描述的非常清楚,Gradle由于重复文件而无法完成构建。我们可以通过exclude LICENTSE文件来修复此问题:

 //You can ignore those files in your build.gradle:
android {
    packagingOptions {
       exclude 'LICENSE.txt'
    }
}

我们可以在应用程序目录下的build/outputs/reports/ androidTests/connected下找到生成的测试报告。 打开index.html查看报表,如下所示:

功能测试报告会显示运行测试的设备和Android版本。我们可以在多个设备上同时运行这些测试,因此这些信息可以帮助我们更容易找到特定设备或特定Android版本的错误。

如果想在Android Studio中获得有关测试的反馈,我们只需要设置 run/debug 属性,以便直接从IDE运行测试。 run/debug 配置表示一组运行或调试程序的启动属性。Android Studio工具栏配置选择器,我们可以在其中选择要使用的 run/debug 配置。

要设置新配置,请单击 Edit Configurations... 打开配置编辑器,然后创建一个新的Android测试配置。选择模块并将instrumentation runner指定AndroidJUnitRunner,如下图所示:

保存此新配置后,可以在配置选择器中选择它,然后单击Run按钮运行所有测试。

从Android Studio运行Espresso测试有一个警告: "the test report is not generated." 。原因是Android Studio执行connectedAndroidTest任务而不是connectedCheck,而connectedCheck是负责生成测试报告的任务。

测试覆盖率

一旦我们开始为Android项目编写测试,代码测试覆盖率将会成为代码测试量的一个重要指标。 Java有很多测试覆盖工具,但Jacoco是最受欢迎的工具。在默认情况下Gradle使用Jacoco作为测试覆盖工具,并且上手也非常容易。

Jacoco

启用覆盖报告非常容易。我们只需要在要测试的构建类型上设置 testCoverageEnabled = true 。如:启用Debug Build Type的测试覆盖范围,如下所示:

 buildTypes {
     debug {
       testCoverageEnabled = true
     }
}

启用测试覆盖时,将在我们执行 gradlew connectedCheck 时创建测试覆盖率报告。创建报告的任务本身是 createDebugCoverageReport 。即使它没有记录,并且在运行gradlew任务时它不会出现在任务列表中,我们也可以直接运行它。 但是,因为 createCoverageReport 依赖于 connectedCheck ,所以不能单独执行它们。 对 connectedCheck 的依赖也意味着我们需要一个连接的设备或模拟器来生成测试覆盖率报告。

任务执行后,我们可以在 app/build/outputs/reports/coverage/debug/index.html 目录中找到coverage报告。每个Build Variant都有自己的报告目录,因为每个variant都可以有不同的测试。测试覆盖率报告样式如下所示:

该报告对于Class级别的覆盖率有一个很好的概述,并且你可以点击以获取更多信息。在最详细的视图中,你可以看到那些代码被测试了,那些没有。

如果要指定特定版本的Jacoco,只需向buildtype添加一个Jacoco配置块,定义版本:

jacoco {
     toolVersion = "0.7.1.201405082137"
}

总结

在本章中,我们学习到了测试Android应用和库的几种方法。 开始了简单的单元测试,然后学了更多与Android相关的测试,还有第三方的Robolectric测试框架。介绍了功能测试和使用Espresso。 最后,研究了如何实现测试覆盖报告。 现在我们知道如何使用Gradle和Android Studio运行整个测试套件,并且可以生成覆盖率报告。

我们还将在在第8章“设置连续集成”中,学习到使用持续集成工具自动化测试。

下一章将介绍自定义构建过程的一个最重要的方面:创建自定义任务和插件,其中包括对Groovy的简要介绍,学习基础的groovy语言知识,不仅有助于创建任务和插件,而且还将更容易理解Gradle的工作原理,我们下个章节见。