Gradle for Android(六) 运行测试

为了确保app或者library的质量,自动化测试非常重要。很长一段时间,ADT缺少对自动化测试的支持,但是最近,Google花费了很大的努力帮助开发者进行测试。一些老的框架进行了升级,并添加了一些新的框架,以确保我们可以彻底测试app和library。我们不仅可以在Android Studio中运行它们,也可以使用Gradle在命令行运行。

本章,我们会探索测试Android app和library的几种方式。我们还将研究Gradle如何帮助实现自动化测试。

本章包含如下内容:

  • 单元测试
  • 功能测试
  • 测试覆盖率

单元测试

好的单元测试不仅能保证工程质量,也可以用来检测新的代码是否会破坏原有功能。Android Studio和Gradle Android插件对单元测试提供了本地支持,你只需要进行少量配置就可以使用。

JUnit

JUnit是一个非常流行的单元测试库,已经有了十多年的历史。它使测试变得简单易懂。需要记住的是,这些流行的单元测试仅对测试业务逻辑有用,不能测试Android SDK相关的代码。

在写测试之前,你需要创建一个目录。按照惯例,目录名称为test,和main目录在同一级别。目录结构如下:

app
└─── src
     ├─── main
     │     ├─── java
     │     │    └─── com.example.app
     │     └───res
     └─── test
           └─── java
                └───com.example.app

随后你可以在src/test/java/com.example.app目录添加测试类。

为了使用JUnit的最新特性,建议使用JUnit4.0以上版本。添加如下依赖:

dependencies {
    testCompile 'junit:junit:4.12'
}

注意我们此处使用testCompile,而不是compile,这可以确保只在运行测试时构建此依赖,正式打包时不会。

如果你的某个构建类型或者product flavors有一些特性,你也可以为这个特定的构建单独添加测试依赖。比如,你只想为付费版添加JUnit测试,可以这样做:

dependencies {
    testPaidCompile '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));
    }
}

要运行所有级别的测试,只需执行gradlew测试。如果你只想在某个构建变体运行测试,只需要添加上这个变体的名字。比如,执行gradlewtestDebug将只在debug变体上运行测试。如果测试失败,Gradle会在命令行中打印错误信息。如果运行成功,Gradle会显示BUILD SUCCESSFUL

单行测试失败将导致真个测试任务失败。这意味着并不是所有的测试都会执行。如果你希望这个测试单元在所有的构建变体上都被执行,添加continue标识:

$ gradlew test --continue

你也可以仅为某个构建变体编写测试,只需要将测试类放在相应的目录就可以了。比如,想要测试付费版本的话,可以将测试类放在src/testPaid/java/com.example.app中。

如果你不想运行整个测试单元,只想测试某个类,可以添加标识:

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

执行测试不仅会运行所有的测试,还会生成一个测试报告,路径为app/build/reports/tests/debug/index.html。测试报告可以帮助排查问题,在自动测试中非常有用。Gradle会为每个构建变体生成一个测试报告。

你也可以在Android Studio中运行测试。在IDE中可以得到及时反馈,并且可以点击失败的测试,从而快速定位到问题代码。

如果你想测试包含Android特有类或者资源引用的代码,常规单元测试是行不通的,这会引发java.lang.RuntimeException: Stub!错误。为了解决这个问题,你可以自己实现Android SDK的每个方法,或者使用一个模拟框架。幸运的是,一些库解决了这个问题,其中最流行的是Robolectric,它提供了简单的方式来测试Android功能,并且不需要设备或者模拟器。

Robolectric

使用Robolectric,你可以编写使用了Android SDK和资源的测试,并在JVM中运行它。也就是说,你不需要设备或者模拟器就可以测试UI组件。

为了使用Robolectric,你需要添加一些依赖。除了Robolectric,你也需要JUnit。如果你使用了Android support library,你还需要Robolectric shadow。

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的文本在点击按钮后发生了变化:

@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 L系统和suport library上存在一些问题。如果你碰到无法找到资源和兼容库相关的错误,可以尝试在模块中添加一个project.properties文件,文件包含如下内容:

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找到兼容库资源。

功能测试

功能测试用于测试应用的几个模块是否如预期那样正常工作。比如,你可以创建一个功能测试,用于确认点击一个按钮是否会打开一个新的页面。Android有几个功能测试框架,最简单易用的是Espresso。

Espresso

Google创建了Espresso,使开发者更容易编写功能测试。它在Android support repository中提供,你可以使用SDK Manager安装。

为了在设备上运行测试,你需要有一个测试运行器。在testing support library中,Google提供了AndroidJUnitRunner测试运行器,它可以帮助你在Android设备上运行JUnit测试类。测试运行器会在设备上安装应用APK和测试APK,运行所有测试,然后根据结果创建测试报告。

假如你已经下载了testing support library,可以如下配置测试运行器:

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

在使用Espresso之前,你还需要配置几个依赖:

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'
}

你需要引用testing support library和espresso-core才能使用Espresso。最后一个依赖,espresso-contrib,是Espresso的一些补充特性,但不是核心库的一部分。

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

如果这个时候尝试运行测试,你会得到下面的错误:

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将不能完成构建。幸运的是,它仅是一个许可描述,所以我们可以在构建中移除。这个错误本身也包含如何修复的信息:

You can ignore those files in your build.gradle:

android {
    packagingOptions {
        exclude 'LICENSE.txt'
    }
}

配置好构建文件之后,你就可以添加测试了。功能测试和常规的单元测试放在不同的目录下。正如依赖配置一样,你需要使用androidTest代替test,正确的功能测试目录是src/androidTest/java/com.example.app。下面是一个测试类的例子,用于检测MainActivityTextView中的文本是否正确。

@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测试之前,你需要确保有一个设备或者模拟器。如果你忘记连接设备,运行测试任务会引发如下异常:

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

当你连接上设备或者启动了模拟器之后,你可以使用gradlew connectedCheck来运行Espresso测试。这个任务会执行connectedAndroidTestcreateDebugCoverageReport,前者会在所有连接的设备上为debug变体运行所有测试,后者会生成一个测试报告。

你可以在应用的build/outputs/reports/androidTests/connected目录找到生成的测试报告。

功能测试报告会显示运行测试的设备以及Android版本。你可以同时在多个设备上运行测试,所以设备信息可以很容易的查看特定设备或者特定版本的问题。

如果你想在Android Studio中得到测试的反馈,你需要设置run/debug配置。run/debug配置代表一组run/debug启动属性。Android Studio的工具栏有一个配置选择器,你可以选择想要使用的run/debug配置。

图1 Android Studio run/debug配置选择器

你可以点击Edit Configurations…打开配置编辑器,创建一个新的Android测试配置。选择测试模块,设备运行器设置为AndroidJUnitRunner

图2 编辑配置窗口

将新配置保存后,你就可以在配置选择器中选择它,然后点击Run按钮来运行所有测试。

在Android Studio中运行Espresso测试有一个问题:不会生成测试报告。这是因为Android Studio执行connectedAndroidTest任务,而不是connectedCheck任务,而测试报告是由connectedCheck生成的。

测试覆盖率

为Android工程编写测试后,需要知道测试在代码库中的覆盖率。Java有很多测试覆盖率工具,最流行的是Jacoco,它也是默认支持的,很容易上手。

Jacoco

启用覆盖率报告非常简单。你只需要在测试的构建类型中设置testCoverageEnabled = true

buildTypes {
    debug {
        testCoverageEnabled = true
    }
}

启用测试覆盖率后,执行gradlew connectedCheck会生成覆盖率报告。真正生成报告的是createDebugCoverageReport。即使它没有被列入文档,执行gradlew tasks时也不会出现在任务列表中,但你可以直接运行它。而由于createCoverageReport依赖connectedCheck,所以你不可以单独运行它们。依赖connectedCheck也表明你需要连接设备或者模拟器来生成测试覆盖率报告。

执行任务之后,你可以在app/build/outputs/reports/coverage/debug/index.html路径找到覆盖率报告。每个构建变体有各自的目录。

如果你想设置一个特定版本的Jacoco,只需:

jacoco {
    toolVersion = "0.7.1.201405082137"
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,980评论 25 707
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,417评论 2 45
  • 我今天一边煮面,一边配料,一边在回想舅舅昨晚说的《一分钟》,悟到自己在工作时,如能把自己所有的灵魂协调到最佳状态,...
    臣服于道的村民阅读 389评论 0 0
  • 月亮双鱼的妹子找我咨询感情问题,她的故事是,伴侣是一个理性的人,一开始相爱时她很欣赏对方能在她被情感淹没的时候帮助...
    封子末阅读 4,528评论 0 3
  • 无聊的时候 抄写着“从此山水不相逢” 洗澡的时候 单曲循环马老五的《Don't wanna know》 可是夜深的...
    黎暖苏阅读 182评论 0 0