首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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目录同级。这个目录结构如下:

代码语言:javascript
复制
app
└─── src
      ├─── main
      │     ├─── java
      │     │    └─── com.example.app
      │     └───res
      └─── test
             └─── java
                  └─── com.example.app

你可以创建一个测试的Class在src/test/java/com.example.app中 为了使用最新的JUnit,可以使用JUnit版本4,在test构建中添加如下依赖关系:

代码语言:javascript
复制
dependencies {
       testCompile 'junit:junit:4.12'
}

值得注意的是,我们使用testCompile,而不是compile。使用testCompile会保证只有在tests中该依赖才会被构建进去,而其他的版本则不会。在Dependencies中加入testCompile不会在Release的APK中编译,如果需要在一些特殊的BuildType或者ProductFlavors中加入配置,那么可以使用test-only的依赖来指定构建。

例如,如果你希望在付费版本加入JUnit测试,可以添加如下代码块:

代码语言:javascript
复制
dependencies {
       testPaidCompile 'junit:junit:4.12'
}

当所有的事情都设置好了后,就是时候开始写一些单元测试了。以下为一个添加两个数字的test函数。

代码语言:javascript
复制
   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:

代码语言:javascript
复制
$ gradlew test --continue

我们也可以通过在一个正确的路径保存一个Test的类来在某个版本中执行Test任务。例如:如果我们希望在付费版本中测试特定的功能,则将该类文件放入src/testPaid/java/com.example.app目录下。

如果你不想执行整个测试流程,而只是执行一个特定的测试类,你可以使用test标志位:

代码语言:javascript
复制
$ 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的话,你也需要使用Robolectricshadow-support类:

代码语言:javascript
复制
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点击后修改文案:

代码语言:javascript
复制
@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:

代码语言:javascript
复制
defaultConfig {
    testInstrumentationRunner"android.support.test.runner.AndroidJUnitRunner"
}

我们在使用Espresso前配置一些依赖关系:

代码语言:javascript
复制
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。这也就是单元测试和功能测试之间的区别。

如果你现在执行这些测试构建,则会出现以下错误:

代码语言:javascript
复制
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中配置该选项:

代码语言:javascript
复制
android {
      packagingOptions {
           exclude 'LICENSE.txt'
      }
}

一旦build.gradle文件配置完成后,就可以开始添加测试单元了。功能测试和常规的单元测试不同,它存放于一个其他的目录。就像依赖配置一样,我们需要使用androidTest取代test,所以正确的功能测试目录为src/androidTest/java/com.example.app

例如,这个测试类检查是否这个TextView中的Text是否在MainActivity中:

代码语言:javascript
复制
@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测试之前,需要确保有一个设备或者模拟器连上了。如果没有连接设备执行该任务的话,则会报错:

代码语言:javascript
复制
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来查看这个报告:

Report

功能测试报告会展示Device和Android的版本。你可以同时在多个设备上执行这些测试任务,所以这些设备信息会更好的查找到设备或者版本单独的Bug。

如果你希望通过Android Studio来获取测试反馈,可以通过IDE直接在run/denig的配置中设置。Android Studio ToolBar上有一个Configuration选项:

Edit Configuration

我们可以在Edit Configurations中设置一个新的Configuration,并且创建一个新的Android测试配置。选择Module并且指定instrumentation runner为AndroidJUnitRunner,如下图所示:

Espresso Configuration

一旦保存了配置后,就可以点击Run启动测试任务。

Test coverage

一旦你开始了Android Project的测试任务,它可以很方便的知道代码被多少测试单元覆盖。Jacoco是最受欢迎的测试工具。

Jacoco

覆盖率报告是否生效是非常容易的,只需要在Build Type中设置testCoverageEnabled = true即可。例如:

代码语言:javascript
复制
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。覆盖率测试报告如下:

Report

如果希望指定一个特殊的版本,那么在Build Type的配置代码块中加入Jacoco的版本定义:

代码语言:javascript
复制
jacoco {
     toolVersion = "0.7.1.201405082137"
}

然而,Jacoco不需要显式的指定一个版本,Jacoco也可以工作。

下一篇
举报
领券