为了确保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
。下面是一个测试类的例子,用于检测MainActivity
的TextView
中的文本是否正确。
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TestingEspressoMainActivityTest {
@Rule
public ActivityTestRule 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测试。这个任务会执行connectedAndroidTest
和createDebugCoverageReport
,前者会在所有连接的设备上为debug变体运行所有测试,后者会生成一个测试报告。
你可以在应用的build/outputs/reports/androidTests/connected
目录找到生成的测试报告。
功能测试报告会显示运行测试的设备以及Android版本。你可以同时在多个设备上运行测试,所以设备信息可以很容易的查看特定设备或者特定版本的问题。
如果你想在Android Studio中得到测试的反馈,你需要设置run/debug配置。run/debug配置代表一组run/debug启动属性。Android Studio的工具栏有一个配置选择器,你可以选择想要使用的run/debug配置。
你可以点击Edit Configurations…打开配置编辑器,创建一个新的Android测试配置。选择测试模块,设备运行器设置为AndroidJUnitRunner
。
将新配置保存后,你就可以在配置选择器中选择它,然后点击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"
}