@Author:彭海波
前言
单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构之后的正确性。但是单元测试并不一定保证程序功能是正确的,更不保证整体业务是准备的。
在现代软件工程中,单元测试已经是软件开发不可或缺的一部分。良好的单元测试技术对软件开发至关重要,可以说它是软件质量的第一关,是软件开发者对软件质量做出的承诺。敏捷开发中尤其强调单元测试的重要性。
单元测试框架
Junit框架
android中的测试框架是扩展的junit,所以在学习android中的单元测试签,可以先去Junit的官方网站熟悉Junit的使用。目前主流的有JUnit3和JUnit4。JUnit3中,测试用例需要继承TestCase类。JUnit4中,测试用例无需继承TestCase类,只需要使用@Test等注解。
使用之前要在工程中加入Junit的依赖,以Gradle build方式为例:
testCompile 'junit:junit:4.10'
下面是一个Junit4的实例:
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
public class Junit4TestCase {
@BeforeClass
public static void setUpBeforeClass() {
System.out.println("Set up before class");
}
@Before
public void setUp() throws Exception {
System.out.println("Set up");
}
@Test
public void testMathPow() {
System.out.println("Test Math.pow");
Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);
}
@Test
public void testMathMin() {
System.out.println("Test Math.min");
Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);
}
// 期望此方法抛出NullPointerException异常
@Test(expected = NullPointerException.class)
public void testException() {
System.out.println("Test exception");
Object obj = null;
obj.toString();
}
// 忽略此测试方法
@Ignore
@Test
public void testMathMax() {
Assert.fail("没有实现");
}
// 使用“假设”来忽略测试方法
@Test
public void testAssume(){
System.out.println("Test assume");
// 当假设失败时,则会停止运行,但这并不会意味测试方法失败。
Assume.assumeTrue(false);
Assert.fail("没有实现");
}
@After
public void tearDown() throws Exception {
System.out.println("Tear down");
}
@AfterClass
public static void tearDownAfterClass() {
System.out.println("Tear down After class");
}
}
Android单元测试框架
Android单元测试的框架关系如下:从上图的类关系图中可以知道,通过android测试类可以实现对android中相关重要的组件进行测试(如Activity,Service,ContentProvider,甚至是application)。
JUnit TestCase类
继承自JUnit的TestCase,不能使用Instrumentation框架。但这些类包含访问系统对象(如Context)的方法。使用Context,你可以浏览资源,文件,数据库等等。基类是AndroidTestCase,一般常见的是它的子类,和特定组件关联。
子类有:
- ApplicationTestCase——测试整个应用程序的类。它允许你注入一个模拟的Context到应用程序中,在应用程序启动之前初始化测试参数,并在应用程序结束之后销毁之前检查应用程序。
- ProviderTestCase2——测试单个ContentProvider的类。因为它要求使用MockContentResolver,并注入一个IsolatedContext,因此Provider的测试是与OS孤立的。
- ServiceTestCase——测试单个Service的类。你可以注入一个模拟的Context或模拟的Application(或者两者),或者让Android为你提供Context和MockApplication。
Instrumentation TestCase类
继承自JUnit TestCase类,并可以使用Instrumentation框架,用于测试Activity。使用Instrumentation,Android可以向程序发送事件来自动进行UI测试,并可以精确控制Activity的启动,监测Activity生命周期的状态。
基类是InstrumentationTestCase。它的所有子类都能发送按键或触摸事件给UI。子类还可以注入一个模拟的Intent。
子类有:
- ActivityTestCase——Activity测试类的基类。
- SingleLaunchActivityTestCase——测试单个Activity的类。它能触发一次setup()和tearDown(),而不是每个方法调用时都触发。如果你的测试方法都是针对同一个Activity的话,那就使用它吧。
- SyncBaseInstrumentation——测试Content Provider同步性的类。它使用Instrumentation在启动测试同步性之前取消已经存在的同步对象。
- ActivityUnitTestCase——对单个Activity进行单一测试的类。使用它,你可以注入模拟的Context或Application,或者两者。它用于对Activity进行单元测试。不同于其它的Instrumentation类,这个测试类不能注入模拟的Intent。
- ActivityInstrumentationTestCase2——在正常的系统环境中测试单个Activity的类。你不能注入一个模拟的Context,但你可以注入一个模拟的Intent。另外,你还可以在UI线程(应用程序的主线程)运行测试方法,并且可以给应用程序UI发送按键及触摸事件。
测试代码示例
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
private static final String TAG = "=== MainActivityTest";
private Instrumentation mInstrument;
private MainActivity mActivity;
private View mToLoginView;
public MainActivityTest() {
super("yuan.activity", MainActivity.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
mInstrument = getInstrumentation();
// 启动被测试的Activity
mActivity = getActivity();
mToLoginView = mActivity.findViewById(yuan.activity.R.id.to_login);
}
public void testPreConditions() {
// 在执行测试之前,确保程序的重要对象已被初始化
assertTrue(mToLoginView != null);
}
//mInstrument.runOnMainSync(new Runnable() {
// public void run() {
// mToLoginView.requestFocus();
// mToLoginView.performClick();
// }
//});
@UiThreadTest
public void testToLogin() {
// @UiThreadTest注解使整个方法在UI线程上执行,等同于上面注解掉的代码
mToLoginView.requestFocus();
mToLoginView.performClick();
}
@Suppress
public void testNotCalled() {
// 使用了@Suppress注解的方法不会被测试
Log.i(TAG, "method 'testNotCalled' is called");
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
}
Robolectric单元测试框架
Instrumentation 与 Roboletric 都是针对 Android 进行单元测试的框架,前者在执行 case 时候是以 Android JUnit 的方式运行,因此必须在真实的 Android 环境中运行(模拟器或者真机),而后者则是以 Java Junit 的方式运行,这里就脱离了对 Android 环境的依赖,而可以直接将 case 在 JVM 中运行,大赞~,因此很适合将 Roboletric 用于 Android 的测试驱动开发。
下面介绍用Robolectric框架进行单元测试的方法,假设我们有一个RobolectricDemo的Android工程,我们要对该工程进行单元测试。
配置Gradle
- 配置 RoboletricDemo/build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
mavenLocal()
mavenCentral()
}
}
- 配置 app/build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "23.0.0 rc2"
defaultConfig {
applicationId "com.pingan.robolectricdemo"
minSdkVersion 15
targetSdkVersion 19
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
testCompile 'junit:junit:4.10'
testCompile 'org.assertj:assertj-core:1.7.0'
testCompile 'org.robolectric:robolectric:3.0'
compile files('libs/AndroidHyperion_1.0.0_release.jar')
testCompile 'com.squareup.okhttp:mockwebserver:2.4.0'
}
- 配置 Build Variants
在 Build Variants 面板中选择 Unit Tests
- 完成添加依赖
打开 Gradle 面板,点击刷新按钮
- 完成之后可以在看到成功添加的所有依赖
完成 Test Case
- 重命名 app/src/androidTest 为 test,并且删除创建项目时自动生成的 ApplicationTest
- 在 MainActivity 中快速创建测试类,选择 JUnit 4,会自动创建 MainActivityTest 至之前修改的 test 目录下
- 编写 Test Case,这里直接贴上测试代码,代码都相当简单
package com.pingan.robolectricdemo;
import android.test.InstrumentationTestCase;
import android.widget.Button;
import android.widget.TextView;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
/**
* Created by hyper on 15/8/13.
*/
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest extends InstrumentationTestCase {
//引用待测Activity
private MainActivity mainActivity;
//引用待测Activity中的TextView和Button
private TextView textView;
private Button button;
@Before
public void setUp() throws Exception{
//获取待测Activity
mainActivity = Robolectric.setupActivity(MainActivity.class);
//初始化textView和button
textView = (TextView)mainActivity.findViewById(R.id.textView);
button = (Button)mainActivity.findViewById(R.id.button);
}
@After
public void tearDown() throws Exception{
}
@Test
public void testInit() throws Exception{
assertNotNull(mainActivity);
assertNotNull(textView);
assertNotNull(button);
//判断包名
assertEquals("com.pingan.robolectricdemo",mainActivity.getPackageName());
//判断textView默认显示的内容
assertEquals("Hello world!", textView.getText().toString());
}
@Test
public void testButton() throws Exception{
//点击Button
button.performClick();
assertEquals("Hyper",textView.getText().toString());
//assertNotNull(textView.getText());
}
@Test
public void testFail() throws Exception{
fail("This case failed");
}
}
Run Test Case
- 打开 Gradle 面板,在面板中执行测试
- 右键 MainActivityTest > Run 'MainActivityTest'
- 在终端中运行 ./gradlew test
查看报告
执行完测试之后,会在 app/build/reports/tests/目录下生成相应地测试报告,使用浏览器打开
Assert
- Junit3和Junit4都提供了一个Assert类(虽然package不同,但是大致差不多)。Assert类中定义了很多静态方法来进行断言。列表如下:
- assertTrue(String message, boolean condition) 要求condition == true
- assertFalse(String message, boolean condition) 要求condition == false
- fail(String message) 必然失败,同样要求代码不可达
- assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)
- assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
- assertNotNull(String message, Object object) 要求object!=null
- assertNull(String message, Object object) 要求object==null
- assertSame(String message, Object expected, Object actual) 要求expected == actual
- assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
- assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true
单元测试方案
Mock/Stub
Mock和Stub是两种测试代码功能的方法。Mock测重于对功能的模拟。Stub测重于对功能的测试重现。比如对于List接口,Mock会直接对List进行模拟,而Stub会新建一个实现了List的TestList,在其中编写测试的代码。
强烈建议优先选择Mock方式,因为Mock方式下,模拟代码与测试代码放在一起,易读性好,而且扩展性、灵活性都比Stub好。
比较流行的Mock有:
其中EasyMock和Mockito对于Java接口使用接口代理的方式来模拟,对于Java类使用继承的方式来模拟(也即会创建一个新的Class类)。Mockito支持spy方式,可以对实例进行模拟。但它们都不能对静态方法和final类进行模拟,powermock通过修改字节码来支持了此功能。
使用Mockito进行单元测试
介绍
Mockito是Google Code上的一个开源项目,Api相对于EasyMock更好友好。与EasyMock不同的是,Mockito没有录制过程,只需要在“运行测试代码”之前对接口进行Stub,也即设置方法的返回值或抛出的异常,然后直接运行测试代码,运行期间调用Mock的方法,会返回预先设置的返回值或抛出异常,最后再对测试代码进行验证。可以查看此文章了解两者的不同。
官方提供了很多样例,基本上包括了所有功能,可以去看看。
引入方法
在你的Gradle文件中加入下面的依赖:
repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:1.9.5" }
示例
这里从官方样例中摘录几个典型的:
- 验证调用行为
import static org.mockito.Mockito.*;
//创建Mock
List mockedList = mock(List.class);
//使用Mock对象
mockedList.add("one");
mockedList.clear();
//验证行为
verify(mockedList).add("one");
verify(mockedList).clear();
- 对Mock对象进行Stub
//也可以Mock具体的类,而不仅仅是接口
LinkedList mockedList = mock(LinkedList.class);
//Stub
when(mockedList.get(0)).thenReturn("first"); // 设置返回值
when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出异常
//第一个会打印 "first"
System.out.println(mockedList.get(0));
//接下来会抛出runtime异常
System.out.println(mockedList.get(1));
//接下来会打印"null",这是因为没有stub get(999)
System.out.println(mockedList.get(999));
// 可以选择性地验证行为,比如只关心是否调用过get(0),而不关心是否调用过get(1)
verify(mockedList).get(0);
使用Mockito测试异步方法
package com.paic.hyperion.core.hfasynchttp.http;
import android.app.Application;
import android.test.ApplicationTestCase;
import org.apache.http.Header;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
@Mock
private AsyncHttpClient mockClient;
@Before
public void setUp(){
MockitoAnnotations.initMocks(this);
mockClient = new AsyncHttpClient();
}
public class ResponseHandler extends AsyncHttpResponseHandler {
private byte[] receive_data;
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] binaryData) {
System.out.println("success");
receive_data = binaryData;
}
@Override
public void onFailure(int statusCode, Header[] headers, byte[] binaryData, Throwable error) {
System.out.println("fail");
}
public String getResult(){
return receive_data.toString();
}
}
@Test
private void test(){
//assertFalse(true);
String result = "hello,world";
final int statusCode = 200;
final Header[] headers ={};
final byte[] binaryData = "hello,world".getBytes();
String url = "https://www.baidu.com";
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
( (ResponseHandler) invocationOnMock.getArguments()[0]).onSuccess(statusCode,headers,binaryData);
return null;
}
}).when(mockClient.get(url,any(ResponseHandler.class)));
ResponseHandler handler = new ResponseHandler();
mockClient.get(url,handler);
assertEquals(handler.getResult(),result);
}
}
使用MockWebServer模拟服务端
介绍
我们的Android应用程序经常要从后端获取数据来进行相关交互,但前端和后台的开发往往是分开进行的,如果后台没有开发完成,前端甚至都无法进行调试,这是很浪费时间的。我们这里引入一个Mockwebserver的库,他可以模拟一个服务,对HTTP和HTTPS的请求返回指定的数据,从而用来验证我们的应用程序是否达到预期效果。你可以确定你正在进行的测试都走的是完整的HTTP协议栈。你甚至可以从真正的web服务器复制HTTP响应来创建你的测试案例。甚至,你还可以在代码中生成比较难以重现的类似500的错误或缓慢的加载响应的情况。详细内容可以参考Github上mockwebserver的介绍。
使用方法
我们可以像使用Mockito一样来使用MockWebServer,使用步骤如下:
- 在你的gradle文件中加入
testCompile 'com.squareup.okhttp:mockwebserver:2.4.0'
- 设计Mock脚本
- 运行你的应用程序
- 验证返回结果
示例
public void test() throws Exception {
// Create a MockWebServer. These are lean enough that you can create a new
// instance for every unit test.
MockWebServer server = new MockWebServer();
// Schedule some responses.
server.enqueue(new MockResponse().setBody("hello, world!"));
server.enqueue(new MockResponse().setBody("sup, bra?"));
server.enqueue(new MockResponse().setBody("yo dog"));
// Start the server.
server.start();
// Ask the server for its URL. You'll need this to make HTTP requests.
URL baseUrl = server.getUrl("/v1/chat/");
// Exercise your application code, which should make those HTTP requests.
// Responses are returned in the same order that they are enqueued.
Chat chat = new Chat(baseUrl);
chat.loadMore();
assertEquals("hello, world!", chat.messages());
chat.loadMore();
chat.loadMore();
assertEquals(""
+ "hello, world!\n"
+ "sup, bra?\n"
+ "yo dog", chat.messages());
// Optional: confirm that your app made the HTTP requests you were expecting.
RecordedRequest request1 = server.takeRequest();
assertEquals("/v1/chat/messages/", request1.getPath());
assertNotNull(request1.getHeader("Authorization"));
RecordedRequest request2 = server.takeRequest();
assertEquals("/v1/chat/messages/2", request2.getPath());
RecordedRequest request3 = server.takeRequest();
assertEquals("/v1/chat/messages/3", request3.getPath());
// Shut down the server. Instances cannot be reused.
server.shutdown();
}
MockResponse
Mock默认返回一个空的response body和一个200的状态码,你可以自定义body的内容(可以是字符串,数组,json等),你还可以通过fluent builder API来对你的响应添加headers
MockResponse response = new MockResponse()
.addHeader("Content-Type", "application/json; charset=utf-8")
.addHeader("Cache-Control", "no-cache")
.setBody("{}");
MockResponse还可以用来模拟慢速网络,这样你能通过设置延迟来测试超时或者弱网
response.throttleBody(1024, 1, TimeUnit.SECONDS);
RecordedRequest
我们可以通过RecordedRequest来检查发送过来的请求的method, path, HTTP version, body, 和headers。
RecordedRequest request = server.takeRequest();
assertEquals("POST /v1/chat/send HTTP/1.1", request.getRequestLine());
assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
assertEquals("{}", request.getUtf8Body());
Dispatcher
默认情况下,MockWebServer使用队列的方式来处理请求,但我们还有另外一种方式来处理请求,就是使用Dispatcher,它根据请求路径来过滤并分发响应结果。
final Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
if (request.getPath().equals("/v1/login/auth/")){
return new MockResponse().setResponseCode(200);
} else if (request.getPath().equals("v1/check/version/")){
return new MockResponse().setResponseCode(200).setBody("version=9");
} else if (request.getPath().equals("/v1/profile/info")) {
return new MockResponse().setResponseCode(200).setBody("{\\\"info\\\":{\\\"name\":\"Lucas Albuquerque\",\"age\":\"21\",\"gender\":\"male\"}}");
}
return new MockResponse().setResponseCode(404);
}
};
server.setDispatcher(dispatcher);
使用DBUnit进行数据库单元测试
简介
DbUnit 是专门针对数据库测试的对JUnit的一个扩展,它可以将测试对象数据库置于一个测试轮回之间的状态。熟悉单元测试的开发人员都知道,在对数据库进行单元测试时候,通常采用的方案有运用模拟对象(mock objects) 和stubs 两种。通过隔离关联的数据库访问类,比如JDBC 的相关操作类,来达到对数据库操作的模拟测试。然而某些特殊的系统,比如利用了EJB 的CMP(container-managed persistence) 的系统,数据库的访问对象是在最底层而且很隐蔽的,那么这两种解决方案对这些系统就显得力不从心了。
DBUnit的设计理念就是在测试之前,备份数据库,然后给对象数据库植入我们需要的准备数据,最后,在测试完毕后,读入备份数据库,回溯到测试前的状态;而且又因为DBUnit 是对JUnit 的一种扩展,开发人员可以通过创建测试用例代码,在这些测试用例的生命周期内来对数据库的操作结果进行比较。
DbUnit 测试基本概念和流程
基于DbUnit 的测试的主要接口是IDataSet 。IDataSet 代表一个或多个表的数据。
可以将数据库模式的全部内容表示为单个IDataSet 实例。这些表本身由Itable 实例来表示。
IDataSet 的实现有很多,每一个都对应一个不同的数据源或加载机制。最常用的几种 IDataSet 实现为:
- FlatXmlDataSet :数据的简单平面文件 XML 表示
- QueryDataSet :用 SQL 查询获得的数据
- DatabaseDataSet :数据库表本身内容的一种表示
- XlsDataSet :数据的excel 表示
一般而言,使用DbUnit 进行单元测试的流程如下: - 根据业务,做好测试用的准备数据和预想结果数据,通常准备成xml 格式文件。
- 在setUp() 方法里边备份数据库中的关联表。
- 在setUp() 方法里边读入准备数据。
- 对测试类的对应测试方法进行实装: 执行对象方法,把数据库的实际执行结果和预想结果进行比较。
- 在tearDown() 方法里边, 把数据库还原到测试前状态。
DbUnit 开发实例
下面通过一个实例来说明DbUnit 的实际运用。
比如有一个学生表[student] ,结构如下:
id char(4) pk 学号
name char(50) 姓名
sex char(1) 性别
birthday date 出生日期
1 准备数据如下:
id | name | sex | birthday |
---|---|---|---|
0001 | 翁仔 | m | 1979-12-31 |
0002 | 王翠花 | f | 1982-08-09 |
测试对象类为StudentOpe.java ,里边有2 个方法:
findStudent(String id) : 根据主键id 找记录
addStudent(Student student) :添加一条记录
在测试addStudent 方法时候,我们准备添加如下一条数据
id | name | sex | birthday |
---|---|---|---|
0088 | 王耳朵 | m | 1982-01-01 |
那么在执行该方法后,数据库的student 表里的数据是这样的:
id | name | sex | birthday |
---|---|---|---|
0001 | 翁仔 | m | 1979-12-31 |
0002 | 王翠花 | f | 1982-08-09 |
0088 | 王耳朵 | m | 1982-01-01 |
然后我们说明如何对这2 个方法进行单元测试。
实例展开
1 把准备数据和预想数据转换成xml 文件
student_pre.xml
<?xml version='1.0' encoding="gb2312"?>
<dataset>
<student id="0001" name=" 翁仔" sex="m" birthday="1979-12-31"/>
<student id="0002" name=" 王翠花" sex="f" birthday="1982-08-09"/>
</dataset>
student_exp.xml
<?xml version='1.0' encoding="gb2312"?>
<dataset>
<student id="0001" name=" 翁仔" sex="m" birthday="1979-12-31"/>
<student id="0002" name=" 王翠花" sex="f" birthday="1982-08-09"/>
<student id="0088" name=" 王耳朵" sex="m" birthday="1982-01-01"/>
</dataset
2 实装setUp 方法,详细见代码注释。
protected void setUp() {
IDatabaseConnection connection =null;
try{
super.setUp();
// 本例使用postgresql 数据库
Class.forName("org.postgresql.Driver");
// 连接DB
Connection conn=DriverManager.getConnection("jdbc:postgresql:testdb.test","postgres","postgres");
// 获得DB 连接
connection =new DatabaseConnection(conn);
// 对数据库中的操作对象表student 进行备份
QueryDataSet backupDataSet = new QueryDataSet(connection);
backupDataSet.addTable("student");
file=File.createTempFile("student_back",".xml");// 备份文件
FlatXmlDataSet.write(backupDataSet,new FileOutputStream(file));
// 准备数据的读入
IDataSet dataSet = new FlatXmlDataSet( new FileInputStream("student_pre.xml"));
DatabaseOperation.CLEAN_INSERT.execute(connection,dataSet);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
if(connection!=null) connection.close();
}catch(SQLException e){}
}
}
3 实装测试方法,详细见代码注释。
- 检索类方法,可以利用assertEquals() 方法,拿表的字段进行比较。
// findStudent
public void testFindStudent() throws Exception{
// 执行findStudent 方法
StudentOpe studentOpe=new StudentOpe();
Student result = studentOpe.findStudent("0001");
// 预想结果和实际结果的比较
assertEquals(" 翁仔",result.getName());
assertEquals("m",result.getSex());
assertEquals("1979-12-31",result.getBirthDay());
}
- 更新,添加,删除等方法,可以利用Assertion.assertEquals() 方法,拿表的整体来比较。
public void testAddStudent() throws Exception{
// 执行addStudent 方法
StudentOpe studentOpe=new StudentOpe();
// 被追加的记录
Student newStudent = new Student("0088"," 王耳朵","m","1982-01-01");
// 执行追加方法
Student result = studentOpe.addStudent(newStudent);
// 预想结果和实际结果的比较
IDatabaseConnection connection=null;
try{
// 预期结果取得
IDataSet expectedDataSet = new FlatXmlDataSet(new FileInputStream("student_exp.xml"));
ITable expectedTable = expectedDataSet.getTable("student");
// 实际结果取得
Connection conn=getConnection();
connection =new DatabaseConnection(conn);
IDataSet databaseDataSet = connection.createDataSet();
ITable actualTable = databaseDataSet.getTable("student");
// 比较
Assertion.assertEquals(expectedTable, actualTable);
}finally{
if(connection!=null) connection.close();
}
}
- 如果在整体比较表的时候,有个别字段不需要比较,可以用DefaultColumnFilter.excludedColumnsTable() 方法,
将指定字段给排除在比较范围之外。比如上例中不需要比较birthday 这个字段的话,那么可以如下代码所示进行处理:
ITable filteredExpectedTable = DefaultColumnFilter.excludedColumnsTable(expectedTable, new String[]{"birthday"});
ITable filteredActualTable = DefaultColumnFilter.excludedColumnsTable(actualTable,new String[]{"birthday"});
Assertion.assertEquals(filteredExpectedTable, filteredActualTable);
4 在tearDown() 方法里边, 把数据库还原到测试前状态
protected void tearDown() throws Exception{
IDatabaseConnection connection =null;
try{
super.tearDown();
Connection conn=getConnection();
connection =new DatabaseConnection(conn);
IDataSet dataSet = new FlatXmlDataSet(file);
DatabaseOperation.CLEAN_INSERT.execute(connection,dataSet);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
if(connection!=null) connection.close();
}catch(SQLException e){}
}
}
关于Android-async-http的单元测试
如果我们直接在AndroidTestCase中对异步请求的方法进行测试,会发现根本没有返回结果,测试就结束了。这是因为Android的单元测试根本不是在主线程跑的,但我们的异步请求创建的Handler并不是绑定到主线程,而是绑定到创建它的线程,即测试线程。这样,测试一结束,Handler也就释放,异步返回就的消息就找不到Handler了。为了解决这个问题,我們發現 ActivityTestCase 擁有一個可以在主線程運行的測試API:runTestOnUiThread,只要将测试代码放进去即可,示例如下:
public class ApplicationTest extends InstrumentationTestCase {
private MockWebServer mServer;
@Override
public void setUp() throws Exception{
mServer = new MockWebServer();
mServer.play();
}
@Override
public void tearDown() throws Exception{
mServer.shutdown();
}
public void testHttp(){
mServer.enqueue(new MockResponse().setResponseCode(200).setBody("hyper"));
final StringBuilder strBuilder = new StringBuilder();
final AsyncHttpClient client = new AsyncHttpClient();
final CountDownLatch signal = new CountDownLatch(1);
final String url = mServer.getUrl("/").toString();
try {
this.runTestOnUiThread(new Runnable() {
@Override
public void run() {
client.get(url, new AsyncHttpResponseHandler() {
@Override
public void onSuccess(int i, Header[] headers, byte[] bytes) {
strBuilder.append(new String(bytes));
}
@Override
public void onFailure(int i, Header[] headers, byte[] bytes, Throwable throwable) {
}
@Override
public void onFinish() {
signal.countDown();
}
});
}
});
} catch (Throwable throwable) {
throwable.printStackTrace();
}
try {
signal.await(3000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
assertEquals(strBuilder.toString(),"hyper");
}
}
代码覆盖率
代码覆盖率的作用主要是用来查看执行完毕后,有哪些代码尚未覆盖到,未覆盖到的代码通常意味着未覆盖到的功能或场景,目前主流的Android覆盖率工具有开源软件Emma和Jacoco。
Emma
- 第一步:把被测工程生成Ant build文件,andriod-app就是工程名
android update project -p android-app - 第二步:将andriod测试工程也转换成ant工程,-m选项指定了测试工程对应的主andriod工程的位置,而android-test就是测试工程名:
android update test-project -m ../android-app -p android-test
- 第三步:执行
下面的命令,编译、执行单元测试、收集覆盖率:
ant clean emma debug install
Jacoco
JaCoCo(Java Code Coverage)是一种分析单元测试覆盖率的工具,使用它运行单元测试后,可以给出代码中哪些部分被单元测试测到,哪些部分没有没测到,并且给出整个项目的单元测试覆盖情况百分比,看上去一目了然。下面介绍一下如何在Android studio中配置Jacoco为单元测试执行覆盖率
在Gradle中加入Jacoco
在build.gradle文件中加入下面的配置项
apply plugin: 'jacoco'
jacoco{
toolVersion = "0.7.5.201505241946"
}
.....
buildTypes {
debug {
testCoverageEnabled = true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
执行Jacoco
先执行单元测试用例,然后执行Jacoco
./gradlew clean createDebugCoverageReport
查看覆盖率结果
总结
关于Android单元测试,个人还是比较推荐Robolectric+Mockito的组合方案。但技术和框架都只是一方面,真正需要推动的是培养开发人员单元测试的意识。对于一个单元测试做得足够好的项目,是不需要担心质量问题的,测试人员应该只需要做质量验收即可。