Gradle For Android系列6:运行测试

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

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

本章主要包括如下内容:

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

单元测试

在项目中编写良好的单元测试不仅可以确保程序的质量,而且还可以方便地检查新代码是否破坏了已完成的功能代码。 Android Studio和Gradle Android插件原生支持单元测试,但是您需要配置一些东西才能使用它们。

JUnit

JUnit是一个非常受欢迎的单元测试库,已经存在了十多年。它使得编写测试变得容易并且易于阅读。请记住,这些特定的单元测试仅用于测试业务逻辑代码,与Android SDK相关的代码是不能通过Junit做测试的。

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

1
2
3
4
5
6
7
8
9
app
|-src
| |-main
| | |-java
| | |-com.example.app
| |-res
|-test
|-java
|-com.example.app

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

要使用JUnit中的最新功能,请使用JUnit 4.x版本,你可以通过添加测试构建的依赖来确保这一点:

1
2
3
dependencies {
testCompile 'junit:junit:4.12'
}

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

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

1
2
3
dependencies {
testPaidCompile 'junit:junit:4.12'
}

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

1
2
3
4
5
6
7
8
9
10
11
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标志:

1
$ gradlew test --continue

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

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

1
$ 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,您需要添加一些测试依赖项。 除了Robolectric本身,你还需要包括JUnit,如果你使用Support Library,你还需要Robolectric shadow class来使用它:

1
2
3
4
5
6
7
8
apply plugin: 'org.robolectric'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
testCompile 'junit:junit:4.12'
testCompile'org.robolectric:robolectric:3.0'
testCompile'org.robolectric:shadows-support:3.0'
}

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

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "app/src/main/AndroidManifest.xml", sdk = 18)
public class MainActivityTest {
@Test
public void clickingButtonShouldChangeText() {
AppCompatActivity activity = Robolectric.buildActivity(MainActivity.class).create().get();
Button button = (Button)activity.findViewById(R.id.button);
TextView textView = (TextView)activity.findViewById(R.id.label);
button.performClick();
assertThat(textView.getText().toString(), equalTo(activity.getString(R.string.hello_robolectric)));
}
}

Robolectric有一些已知的与Android Lollipop和support包的兼容问题。如果你遇到错误:”missing resources related to the compatibility library“,可以按如下办法解决,你需要在项目中添加一个名为project.properties的文件,并向其中添加以下内容:

1
2
3
android.library.reference.1=../../build/intermediates/
exploded-aar/com.android.support/appcompat-v7/22.2.0
android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/support-v4/22.2.0

这将有助于Robolectric找到兼support资源。

功能(UI)测试

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

Espresso

Google创建了Espresso库,以方便开发人员更轻松地编写功能测试。该库通过Android Support Library提供,因此你需要通过SDK Manager安装Espresso库。

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

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

1
2
3
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

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

1
2
3
4
5
6
7
8
9
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
androidTestCompile 'com.android.support.test:runner:0.3'
androidTestCompile 'com.android.support.test:rules:0.3'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2'
}

您需要引用测试的support Library和espresso-core来开始使用Espresso。最后一个依赖项espresso-contrib是一个具有补充Espresso功能的库,但不是核心库的一部分。

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

如果你试图在这一点上运行测试版本,你可能会遇到如下这个错误:

1
2
3
4
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由于重复文件而无法完成构建。幸运的是,它只是一个LICENTSE文件,所以我们可以完全剥离它。错误本身也提示了如何解决这个问题:

1
2
3
4
5
6
7
You can ignore those files in your build.gradle:
android {
packagingOptions {
exclude 'LICENSE.txt'
}
}

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

1
2
3
4
5
6
7
8
9
10
11
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TestingEspressoMainActivityTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void testHelloWorldIsShown() {
onView(withText("Hello world!")).check(matches(isDisplayed()));
}
}

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

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

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

你可以在应用程序目录下的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的测试覆盖范围,如下所示:

1
2
3
4
5
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配置块,定义版本:

1
2
3
jacoco {
toolVersion = "0.7.1.201405082137"
}

总结

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

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

下一章将介绍自定义构建过程的一个最重要的方面:创建自定义任务和插件,其中包括对Groovy的简短介绍。这不仅有助于创建任务和插件,而且还将更容易理解Gradle的工作原理。

写得好,就打赏一下吧!