Gradle For Android(6)--测试单元
介绍
为了保证APP的质量,有一些自动化测试也是很重要的。很长一段时间Android Developement Tools缺少了对自动化测试的支持。但是最近Google让开发者们可以更容易的接入这些测试了。
很多旧的Framework已经升级,而新的Framework也可以保证我们可以在APP和Library中访问这些。我们不仅仅可以在Android Studio中执行这些测试任务,也可以在命令行中执行,比如说通过Gradle。
Unit tests
一个写的好的单元测试不仅仅能够保证质量,它也能让我们检查新的代码是不是会破坏原有的功能性代码。Android Studio和Gradle Android Plugin可以为单元测试提供支持,但是需要我们可以配置一些东西。
JUnit
JUnit是一个常用的单元测试Lib。它可以让写出来的单元测试很容易的理解。值得注意的是,这些特殊的单元测试只对业务逻辑测试有用,而与Android SDK相关的则不会生效。
在使用JUnit写单元测试之前,你需要创建一个为了tests的目录。这个目录可以叫做test
,并且它应该和你的main目录同级。这个目录结构如下:
app
└─── src
├─── main
│ ├─── java
│ │ └─── com.example.app
│ └───res
└─── test
└─── java
└─── com.example.app
你可以创建一个测试的Class在src/test/java/com.example.app
中
为了使用最新的JUnit,可以使用JUnit版本4,在test构建中添加如下依赖关系:
dependencies {
testCompile 'junit:junit:4.12'
}
值得注意的是,我们使用testCompile
,而不是compile
。使用testCompile
会保证只有在tests中该依赖才会被构建进去,而其他的版本则不会。在Dependencies中加入testCompile
不会在Release的APK中编译,如果需要在一些特殊的BuildType或者ProductFlavors中加入配置,那么可以使用test-only
的依赖来指定构建。
例如,如果你希望在付费版本加入JUnit测试,可以添加如下代码块:
dependencies {
testPaidCompile 'junit:junit:4.12'
}
当所有的事情都设置好了后,就是时候开始写一些单元测试了。以下为一个添加两个数字的test函数。
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 test
来执行所有的单元测试。如果你希望在一个Build Variant中来执行这些测试,那么可以添加这个Variant的名字即可。如果只想在Debug版本进行测试,那么就可以执行gradlewtestDebug
。如果单元测试失败了,那么Gradle就会在命令行打印出来失败日志。如果成功了,那么Gradle会打印出来BUILD SUCCESSFUL
的日志。
如果某个test任务失败了,整个过程会立刻终止。也就意味着如果失败,所有的任务都不会执行。如果希望整个test流程都执行完的话,那么可以使用continue
的Flag:
$ gradlew test --continue
我们也可以通过在一个正确的路径保存一个Test的类来在某个版本中执行Test任务。例如:如果我们希望在付费版本中测试特定的功能,则将该类文件放入src/testPaid/java/com.example.app
目录下。
如果你不想执行整个测试流程,而只是执行一个特定的测试类,你可以使用test标志位:
$ gradlew testDebug --tests="*.LogicTest"
执行测试任务不仅仅只会执行Test,也会创建一个Test Report,而这个文件的路径就放在app/build/reports/tests/debug/index.html
。这个Report可以帮助我们查看哪儿失败了,并且对于自动化测试非常有用。Gradle会为每一个Build Variant执行测试任务构建一个Report。
如果test任务执行成功,那么单元测试的报告就会如下:
Unit Test
我们可以直接使用Android Studio执行Test任务。当我们使用的时候,会在IDE中直接反馈,当任务失败的时候,则会出现错误码,如果任务成功的话,那么Run Tool Window会如下所示:
Run Tool Window
如果你想测试部分引用了Android特殊的类和资源的代码的话,那么普通的单元测试则不能使用。当执行这任务的时候,会出现java.lang.RuntimeException: Stub!
错误。为了修复这个错误,我们需要手动实现每个Android SDK的方法,或者使用mocking框架。
幸运的是,一部分Lib已经处理好了Android SDK的问题。Robolectric
这个Lib提供了一个Android功能测试的快捷的方式,并且不需要设备和模拟器。
Robolectric
我们可以使用Robolectric
来编写使用Android SDK和资源的测试。而这些测试任务会跑在一个JVM中。这也就意味着它不需要在设备或者虚拟机上使用Android资源了。因此,这样也会对于APP或者Library的UI组件表现的测试会更加快速。
开始使用Robolectric
之前,我们需要添加一些测试的Dependencies。在Robolectric
之内,也需要包含JUnit,并且如果需要使用Support Library的话,你也需要使用Robolectric
的shadow-support
类:
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在一个Button点击后修改文案:
@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和兼容包中都有一些已知的问题。如果在执行的时候遇到缺失兼容包中的资源的话,可以通过下面的方式修复:
在Module中加入一个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
找到Support中的资源
Functional tests
功能测试用来测试App中的一些组件是否与预期一样进行工作的。例如,你可以创建一个功能性的测试:点击一个Button打开一个新的Activity。Android提供了一些功能性测试的框架,但是最简单的还是使用Espresso
框架。
Espresso
Espresso Library通过Android Support仓库提供。所以可以通过SDK Manager安装。为了在设备上进行测试,我们需要定义一个test runner。通过testing support library,Google提供了一个名为AndroidJUnitRunner
的test runner,它可以帮我们在Android设备上运行JUnit Test类。Test Runner会将App的Apk和test的APK安装到该设备上,并且执行所有的test,然后将test结果生成到report中。
以下是如何设置test runner:
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'
}
我们需要引用test 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不能完成构建,因为有多个相同的文件。幸运的是,它只是一个License描述,所以我们可以在构建中忽略它。这个错误包含了我们应该怎么做,我们可以在build.gradle
中配置该选项:
android {
packagingOptions {
exclude 'LICENSE.txt'
}
}
一旦build.gradle
文件配置完成后,就可以开始添加测试单元了。功能测试和常规的单元测试不同,它存放于一个其他的目录。就像依赖配置一样,我们需要使用androidTest
取代test
,所以正确的功能测试目录为src/androidTest/java/com.example.app
。
例如,这个测试类检查是否这个TextView中的Text是否在MainActivity中:
@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
来运行测试任务。这个任务会和connectedAndroidTest
任务一起执行,在设备上执行Debug Build中的所有测试任务,并且创建DebugCoverageReport
的报告。这个报告会在App目录下的build/outputs/ reports/androidTests/connected
路径中。打开index.html
来查看这个报告:
功能测试报告会展示Device和Android的版本。你可以同时在多个设备上执行这些测试任务,所以这些设备信息会更好的查找到设备或者版本单独的Bug。
如果你希望通过Android Studio来获取测试反馈,可以通过IDE直接在run/denig
的配置中设置。Android Studio ToolBar上有一个Configuration选项:
我们可以在Edit Configurations
中设置一个新的Configuration,并且创建一个新的Android测试配置。选择Module并且指定instrumentation runner为AndroidJUnitRunner,如下图所示:
一旦保存了配置后,就可以点击Run
启动测试任务。
Test coverage
一旦你开始了Android Project的测试任务,它可以很方便的知道代码被多少测试单元覆盖。Jacoco是最受欢迎的测试工具。
Jacoco
覆盖率报告是否生效是非常容易的,只需要在Build Type中设置testCoverageEnabled = true
即可。例如:
buildTypes {
debug {
testCoverageEnabled = true
}
}
当testCoverageEnabled
打开时,执行gradlew connectedCheck
就会生成覆盖率报告。而生成这个报告的任务名为createDebugCoverageReport
。即使它没有在文档中记录,并且也没有在task列表中,而当你执行gradlew tasks
时,它就会直接运行的。
然而,由于createCoverageReport
依赖于connectedCheck
,你不能单独运行这几个任务。connectedCheck
这个任务也需要链接一个模拟器或者设备才能执行,并且生成test coverage report。
当这个任务被执行后,可以在app/build/outputs/reports/coverage/debug/index.html
中找到覆盖率的报告。每一个Build Variant都有自己的覆盖率报告路径,因为每个Variant都有自己不同的tests。覆盖率测试报告如下:
如果希望指定一个特殊的版本,那么在Build Type的配置代码块中加入Jacoco的版本定义:
jacoco {
toolVersion = "0.7.1.201405082137"
}
然而,Jacoco不需要显式的指定一个版本,Jacoco也可以工作。