官方文档链接:https://developer.android.google.cn/training/testing/unit-testing/index.html
1.前言
单元测试是应用程序测试策略中的基本测试。通过对代码创建和运行单元测试,可以轻松验证独立逻辑单元是否正确。每次构建后运行单元测试,有助于快速捕获和修复代码改变引入的软件问题。通常以可重复的方式执行尽可能小的代码单元的功能(可能是方法、类或组件)。当需要验证应用程序中指定代码的逻辑,应该构建单元测试。例如,正在单元测试一个类,可能会检查类的状态是否正确。代码单元是独立测试的,只影响和监听指定单元的改变,模拟框架可被用来隔离单元和它的依赖。
注意:单元测试不适合测试复杂的UI交互事件。应该使用UI测试框架,如自动化UI测试中描述的。
为了测试安卓应用程序,通常会创建这些类型的自动化单元测试:
- 本地测试:只运行于本地机器的单元测试。这些测试编译运行于Java虚拟机(JVM)来减少执行时间,不依赖安卓框架或者可以使用模拟对象替代依赖项。
- 设备测试:运行在安卓设备或模拟器上的单元测试。这些测试可以访问仪器的信息,例如被测试应用程序的上下文,这些不易被模拟对象替代的安卓依赖项。
下面将介绍如何构建这些类型的自动化单元测试。
2.本地单元测试
如果单元测试没有依赖或仅简单依赖安卓,应该在本地开发机器上运行测试。这种方式有助于避免每次运行测试,都加载目标应用程序和单元测试代码到物理设备或模拟器,大大减少单元测试的执行时间。为配合这种方式,通常使用类似Mockito的模拟框架来满足所有依赖关系。
2.1.设置测试环境
在Android Studio项目中,必须存储本地单元测试的源文件到模块名/src/test/java/
目录(创建新项目时已存在)下。还需要使用JUnit 4框架提供的标准APIs,来配置项目的测试依赖。如果测试需要安卓的依赖配合,类似Mockito库可以简化本地单元测试,要了解关于使用模拟对象的更多信息,请参阅模拟Android依赖项。
在应用程序顶层build.gradle
文件中(即工程目录,若仅哪个模块需要,在该模块下配置),需要指定这些库作为依赖:
dependencies {
// Required -- JUnit 4 framework
testCompile 'junit:junit:4.12'
// Optional -- Mockito framework
testCompile 'org.mockito:mockito-core:1.10.19'
}
2.2.创建本地单元测试类
本地单元测试类应该写成JUnit 4测试类。JUnit是最流行和广泛使用的Java单元测试框架,它最新的版本相比之前,允许以更简洁和灵活的方式编写测试。不同于基于JUnit 3的Android单元测试的做法,JUnit 4不需要扩展junit.framework.TestCase
类,也不需要为测试方法名称加test
关键字作为前缀,同时不需要使用junit.framework
或junit.extensions
包中的任何类。
创建基本的JUnit 4测试类(包含一个或多个测试方法的Java类)。每个测试方法以@Test
注解开始,且包含用于执行和验证想要测试的组件中单一功能的代码。下面的例子展示如何实现本地单元测试类,测试方法emailValidator_CorrectEmailSimple_ReturnsTrue
验证被测试的应用程序中isValidEmail()
方法返回结果的正确性。
import org.junit.Test;
import java.util.regex.Pattern;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class EmailValidatorTest {
@Test
public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
assertThat(EmailValidator.isValidEmail("[email protected]"), is(true));
}
...
}
为测试应用程序中的组件是否返回期望的结果,使用junit.Assert
方法执行验证检查(或断言),来比较被测试组件的状态与一些期望的值。为了让测试更具可读性,可以使用Hamcrest匹配器(例如is()
和equalTo()
方法)来比较返回的结果和预期的结果。
2.3.模拟安卓依赖项
默认情况下,Gradle的安卓插件基于修改过的android.jar
库(不包含任何实际代码),执行本地单元测试。在测试方法中调用安卓类时会引发异常,来确保只测试编写的代码,不依赖于Android平台的任何特有行为(当没有显式地模拟时)。
可以使用模拟框架模拟代码中的外部依赖项,从而很容易地按期望的方式测试需要与依赖交互的组件。不仅将单元测试与Android系统的其余部分隔离,同时验证那些依赖项中的正确方法是否被调用。支持Java的Mockito模拟框架(1.9.5版本及以上)提供对安卓单元测试的兼容,可以配置模拟对象被调用时返回一些特定的值。若要使用此框架向本地单元测试添加模拟对象,请遵循以下开发步骤:
- 按照设置测试环境那节中描述的,在
build.gradle
文件中添加对Mockito库的依赖。 - 在单元测试类的定义之前,加上
@RunWith(MockitoJUnitRunner.class)
注解。这个注解告诉Mockito测试运行器去验证框架的使用是否正确,并且简化模拟对象的初始化。 - 为安卓依赖项创建模拟对象时,在成员变量声明前添加
@Mock
注解。 - 重写依赖项的行为,可以通过使用
when()
和thenReturn()
方法,指定一个条件和当条件满足时返回的值。
下面的例子展示如何使用模拟的上下文对象创建单元测试。
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import android.content.SharedPreferences;
@RunWith(MockitoJUnitRunner.class)
public class UnitTestSample {
private static final String FAKE_STRING = "HELLO WORLD";
@Mock
Context mMockContext;
@Test
public void readStringFromContext_LocalizedString() {
// Given a mocked Context injected into the object under test...
when(mMockContext.getString(R.string.hello_word))
.thenReturn(FAKE_STRING);
ClassUnderTest myObjectUnderTest = new ClassUnderTest(mMockContext);
// ...when the string is returned from the object under test...
String result = myObjectUnderTest.getHelloWorldString();
// ...then the result should be the expected one.
assertThat(result, is(FAKE_STRING));
}
}
要了解更多关于使用Mockito框架,请参阅Mockito API参考和样例代码中的SharedPreferencesHelperTest
类。
2.4.Error:"Method ... not mocked"
如果运行测试时,调用了安卓SDK中没有模拟的API,将会接收到一个此方法没有被模拟的错误,这是因为运行单元测试使用的android.jar
文件不包含任何实际代码(这些APIs仅由设备上安卓系统镜像提供)。通过默认引发异常,来确保只测试编写的代码,不依赖于Android平台的任何特有行为(当没有显式地模拟时,例如使用Mockito)。当不希望测试中抛出异常时,可以通过给项目顶层的build.gradle
文件(若仅针对模块,就用模块下的)中添加如下的配置来改变行为,让方法能够返回null或0:
android {
...
testOptions {
unitTests.returnDefaultValues = true
}
}
注意:设置
returnDefaultValues
属性为true时,应当谨慎。以null/0作为返回值会在测试中传递,这很难调试,而且可能会导致失败的测试通过,所以把它当作最后的手段。
2.5.运行本地单元测试
要运行本地单元测试,请执行以下步骤:
- 通过点击工具栏中同步工程按钮,确保项目被Gradle同步。
- 用下面的方式之一运行测试:
- 运行单一测试方法,打开
Project
窗口,然后右击一个测试方法并点击Run
选项。 - 运行类中所有测试方法,右击这个类或测试文件中的方法并点击
Run
选项。 - 运行目录下所有测试方法,右击这个目录并点击
Run tests
选项。
- 运行单一测试方法,打开
Gradle的安卓插件会编译位于默认目录(src/test/java/
)下的本地单元测试代码,构建一个测试应用程序,并且使用默认的测试运行器类来本地执行它,然后Android Studio在Run窗口中展示结果。
3.设备单元测试
设备单元测试是运行在物理设备和模拟器的测试,可以使用安卓框架APIs和支持的APIs,例如安卓测试支持库。当测试需要访问设备信息(例如目标应用程序的上下文)或需要安卓框架组件的真正实现(例如Parcelable或SharedPreferences对象)时,才创建设备单元测试。使用设备单元测试也有助于减少需要编写和维护模拟代码的工作量,同时可以使用模拟框架来模拟任何依赖关系。
3.1.设置测试环境
在Android Studio项目中,必须存储设备测试的源文件到模块名/src/androidTest/java/
目录下,此目录创建新项目时已存在并包含设备测试样例。
首先应该下载安卓测试支持库,它提供为应用程序快速构建和运行设备测试代码的APIs,同时包含JUnit 4测试运行器(AndroidJUnitRunner)和UI功能测试(Espresso和UI Automator)所需的APIs。接着,需要配置工程的安卓测试依赖项,来使用测试支持库提供的测试运行器和规定的APIs。为了简化测试开发,也应该包含Hamcrest库,从而使用它的匹配APIs来创建更灵活的断言。
在应用程序顶层build.gradle
文件中(即工程目录,若仅哪个模块需要,在该模块下配置),需要指定这些库作为依赖:
dependencies {
androidTestCompile 'com.android.support:support-annotations:24.0.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
// Optional -- Hamcrest library
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
// Optional -- UI testing with Espresso
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
// Optional -- UI testing with UI Automator
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}
注意:如果构建配置的依赖包括
compile
注解支持库和androidTestCompile
Espresso核心库,那么依赖冲突可能会导致构建失败。要解决此问题,按下面的方式更新对Espresso核心库的依赖:androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' })
为了使用JUnit 4测试类,通过在app模块下的build.gradle
文件中添加以下设置,确保指定AndroidJUnitRunner作为工程的默认设备测试运行器:
android {
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
3.2.创建设备单元测试类
设备单元测试类应该写成JUnit 4测试类。要了解更多关于创建JUnit 4测试类和使用JUnit 4断言及注解,请参阅创建本地单元测试类。为创建设备的JUnit 4测试类,在定义此类之前添加@RunWith(AndroidJUnit4.class)
注解,也需要指定安卓测试支持库提供的AndroidJUnitRunner类为默认测试运行器。下面例子展示如何编写设备单元测试,来验证LogHistory类是否正确实现了Parcelable接口:
import android.os.Parcel;
import android.support.test.runner.AndroidJUnit4;
import android.util.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class LogHistoryAndroidUnitTest {
public static final String TEST_STRING = "This is a string";
public static final long TEST_LONG = 12345678L;
private LogHistory mLogHistory;
@Before
public void createLogHistory() {
mLogHistory = new LogHistory();
}
@Test
public void logHistory_ParcelableWriteRead() {
// Set up the Parcelable object to send and receive.
mLogHistory.addEntry(TEST_STRING, TEST_LONG);
// Write the data.
Parcel parcel = Parcel.obtain();
mLogHistory.writeToParcel(parcel, mLogHistory.describeContents());
// After you're done with writing, you need to reset the parcel for reading.
parcel.setDataPosition(0);
// Read the data.
LogHistory createdFromParcel = LogHistory.CREATOR.createFromParcel(parcel);
List> createdFromParcelData = createdFromParcel.getData();
// Verify that the received data is correct.
assertThat(createdFromParcelData.size(), is(1));
assertThat(createdFromParcelData.get(0).first, is(TEST_STRING));
assertThat(createdFromParcelData.get(0).second, is(TEST_LONG));
}
}
3.3.创建测试套件
为组织设备单元测试的执行,可以收集一系列测试类到一个测试套件类中,然后一起运行这些测试。测试套件可以被嵌套,即收集其它测试套件到自己测试套件中,然后一起运行所有的测试类。测试套件包含在测试包中,类似于主应用程序包,命名通常以.suite
结尾作为后缀(例如,com.example.android.testing.mysample.suite
)。
为单元测试创建测试套件,需导入JUnit中RunWith和Suite类。在套件中添加@RunWith(Suite.class)
和@Suite.SuitClasses()
注解,并在@Suite.SuitClasses()
注解中分别列出测试类或测试套件作为参数。下面的例子展示,如何实现名为UnitTestSuite的测试套件,它收集并一起运行 CalculatorInstrumentationTest和CalculatorAddParameterizedTest测试类。
import com.example.android.testing.mysample.CalculatorAddParameterizedTest;
import com.example.android.testing.mysample.CalculatorInstrumentationTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
// Runs all unit tests.
@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorInstrumentationTest.class,
CalculatorAddParameterizedTest.class})
public class UnitTestSuite {}
3.4.运行设备单元测试
按照下面这些步骤运行设备测试:
- 通过点击工具栏中同步工程按钮,确保项目被Gradle同步。
- 用下面的方式之一运行测试:
- 运行单一测试方法,打开
Project
窗口,然后右击一个测试方法并点击Run
选项。 - 运行类中所有测试方法,右击这个类或测试文件中的方法并点击
Run
选项。 - 运行目录下所有测试方法,右击这个目录并点击
Run tests
选项。
- 运行单一测试方法,打开
Gradle的安卓插件会编译位于默认目录(src/androidTest/java/
)下的设备测试代码,构建一个测试应用包和产品应用包,安装到连接的设备或模拟器上,并且运行测试,随后Android Studio在Run窗口中展示结果。
注意:当运行或调试设备测试时,Android Studio不注入
Instant Run
所需的额外方法,并将功能关闭。
3.5.在Firebase上运行测试
使用Firebase测试实验室,可以在谷歌数据中心的物理和虚拟设备中,选择多款流行安卓设备和不同配置(地区、横竖屏、屏幕尺寸和平台版本),同时测试应用程序。可以从Android Studio或命令行,直接部署应用程序到测试实验室。测试结果提供日志,并包括应用程序失败的所有详细信息。
在开始使用Firebase测试实验室之前,需要做到以下几点,除非已经拥有谷歌账号和Firebase工程:
- 如果还没有,创建谷歌账号。
- 在Firebase控制台中,点击
Create New Project
选项。
在Spark计划的每日免费额度内,使用测试实验室测试应用程序不收取费用。Android Studio提供集成工具,用来配置希望如何部署测试到Firebase测试实验室。当按照规定的步骤创建完Firebase工程,就可以创建测试配置和运行测试:
- 在主菜单点击
Run > Edit Configurations
选项。 - 点击
Add New Configuration
选项并选择Android Tests
。 - 在安卓测试配置对话框内:
- 输入或选择测试的详细信息,例如测试名称、模块类型、测试类型和测试类。
- 从
Deployment Target Options
功能区的Target
下拉菜单中,选择Firebase Test Lab Device Matrix
选项。 - 如果还没有登录,点击
Connect to Google Cloud Platform
,并允许Android Studio访问自己的账户。 - 接着是
Cloud Project
,点击Settings
按钮并从列表中选择自己的Firebase工程。
- 创建和配置测试矩阵:
- 接着是
Matrix Configuration
下拉列表,点击Open Dialog
按钮。 - 点击
Add New Configuration (+)
。 - 在
Name
字段处,输入新配置的名字。 - 选择想要测试应用程序的设备、安卓版本、区域和横竖屏。Firebase测试实验室将在选择的每种组合下测试应用程序,并生成测试结果。
- 点击
OK
保存配置。
- 接着是
- 点击
Run/Debug Configurations
对话框中的OK
按钮退出。 - 通过点击
Run
按钮运行测试。
当Firebase测试实验室完整运行了测试,Run
窗口将打开并显示结果,如下图所示。可能需要点击Show Passed
按钮来查看所有执行过的测试。
也可以通过点击Run
窗口中,显示在测试执行日志开头的链接,到网页上分析测试。要了解更多关于网页展示结果的分析,请参阅分析Firebase安卓测试实验室的结果。
3.6.附加示例代码
要下载关于设备单元测试的示例应用程序,请参阅Android ActivityInstrumentation Sample。
4.总结
单元测试可以说是程序员在开发时,最常用的自检技术。通过它确定业务逻辑当中,输入和输出的对应关系(与函数式编程观点类似,有兴趣可以研究),方便开发人员确定自己编码的正确性,减少安装到设备上调试的频率,大大提高了工作效率,希望大家可以重视。