@Author:彭海波
前言
單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測試工作。程序單元是應(yīng)用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數(shù)、過程等;對于面向?qū)ο缶幊?,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法?br>
單元測試不僅僅用來保證當(dāng)前代碼的正確性,更重要的是用來保證代碼修復(fù)、改進(jìn)或重構(gòu)之后的正確性。但是單元測試并不一定保證程序功能是正確的,更不保證整體業(yè)務(wù)是準(zhǔn)備的。
在現(xiàn)代軟件工程中,單元測試已經(jīng)是軟件開發(fā)不可或缺的一部分。良好的單元測試技術(shù)對軟件開發(fā)至關(guān)重要,可以說它是軟件質(zhì)量的第一關(guān),是軟件開發(fā)者對軟件質(zhì)量做出的承諾。敏捷開發(fā)中尤其強(qiáng)調(diào)單元測試的重要性。
單元測試框架
Junit框架
android中的測試框架是擴(kuò)展的junit,所以在學(xué)習(xí)android中的單元測試簽,可以先去Junit的官方網(wǎng)站熟悉Junit的使用。目前主流的有JUnit3和JUnit4。JUnit3中,測試用例需要繼承TestCase類。JUnit4中,測試用例無需繼承TestCase類,只需要使用@Test等注解。
使用之前要在工程中加入Junit的依賴,以Gradle build方式為例:
testCompile 'junit:junit:4.10'
下面是一個Junit4的實(shí)例:
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("沒有實(shí)現(xiàn)");
}
// 使用“假設(shè)”來忽略測試方法
@Test
public void testAssume(){
System.out.println("Test assume");
// 當(dāng)假設(shè)失敗時(shí),則會停止運(yùn)行,但這并不會意味測試方法失敗。
Assume.assumeTrue(false);
Assert.fail("沒有實(shí)現(xiàn)");
}
@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單元測試的框架關(guān)系如下:
從上圖的類關(guān)系圖中可以知道,通過android測試類可以實(shí)現(xiàn)對android中相關(guān)重要的組件進(jìn)行測試(如Activity,Service,ContentProvider,甚至是application)。
JUnit TestCase類
繼承自JUnit的TestCase,不能使用Instrumentation框架。但這些類包含訪問系統(tǒng)對象(如Context)的方法。使用Context,你可以瀏覽資源,文件,數(shù)據(jù)庫等等?;愂茿ndroidTestCase,一般常見的是它的子類,和特定組件關(guān)聯(lián)。
子類有:
- ApplicationTestCase——測試整個應(yīng)用程序的類。它允許你注入一個模擬的Context到應(yīng)用程序中,在應(yīng)用程序啟動之前初始化測試參數(shù),并在應(yīng)用程序結(jié)束之后銷毀之前檢查應(yīng)用程序。
- ProviderTestCase2——測試單個ContentProvider的類。因?yàn)樗笫褂肕ockContentResolver,并注入一個IsolatedContext,因此Provider的測試是與OS孤立的。
- ServiceTestCase——測試單個Service的類。你可以注入一個模擬的Context或模擬的Application(或者兩者),或者讓Android為你提供Context和MockApplication。
Instrumentation TestCase類
繼承自JUnit TestCase類,并可以使用Instrumentation框架,用于測試Activity。使用Instrumentation,Android可以向程序發(fā)送事件來自動進(jìn)行UI測試,并可以精確控制Activity的啟動,監(jiān)測Activity生命周期的狀態(tài)。
基類是InstrumentationTestCase。它的所有子類都能發(fā)送按鍵或觸摸事件給UI。子類還可以注入一個模擬的Intent。
子類有:
- ActivityTestCase——Activity測試類的基類。
- SingleLaunchActivityTestCase——測試單個Activity的類。它能觸發(fā)一次setup()和tearDown(),而不是每個方法調(diào)用時(shí)都觸發(fā)。如果你的測試方法都是針對同一個Activity的話,那就使用它吧。
- SyncBaseInstrumentation——測試Content Provider同步性的類。它使用Instrumentation在啟動測試同步性之前取消已經(jīng)存在的同步對象。
- ActivityUnitTestCase——對單個Activity進(jìn)行單一測試的類。使用它,你可以注入模擬的Context或Application,或者兩者。它用于對Activity進(jìn)行單元測試。不同于其它的Instrumentation類,這個測試類不能注入模擬的Intent。
- ActivityInstrumentationTestCase2——在正常的系統(tǒng)環(huán)境中測試單個Activity的類。你不能注入一個模擬的Context,但你可以注入一個模擬的Intent。另外,你還可以在UI線程(應(yīng)用程序的主線程)運(yùn)行測試方法,并且可以給應(yīng)用程序UI發(fā)送按鍵及觸摸事件。
測試代碼示例
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() {
// 在執(zhí)行測試之前,確保程序的重要對象已被初始化
assertTrue(mToLoginView != null);
}
//mInstrument.runOnMainSync(new Runnable() {
// public void run() {
// mToLoginView.requestFocus();
// mToLoginView.performClick();
// }
//});
@UiThreadTest
public void testToLogin() {
// @UiThreadTest注解使整個方法在UI線程上執(zhí)行,等同于上面注解掉的代碼
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 進(jìn)行單元測試的框架,前者在執(zhí)行 case 時(shí)候是以 Android JUnit 的方式運(yùn)行,因此必須在真實(shí)的 Android 環(huán)境中運(yùn)行(模擬器或者真機(jī)),而后者則是以 Java Junit 的方式運(yùn)行,這里就脫離了對 Android 環(huán)境的依賴,而可以直接將 case 在 JVM 中運(yùn)行,大贊~,因此很適合將 Roboletric 用于 Android 的測試驅(qū)動開發(fā)。
下面介紹用Robolectric框架進(jìn)行單元測試的方法,假設(shè)我們有一個RobolectricDemo的Android工程,我們要對該工程進(jìn)行單元測試。
配置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 面板,點(diǎn)擊刷新按鈕

- 完成之后可以在看到成功添加的所有依賴

完成 Test Case
- 重命名 app/src/androidTest 為 test,并且刪除創(chuàng)建項(xiàng)目時(shí)自動生成的 ApplicationTest
- 在 MainActivity 中快速創(chuàng)建測試類,選擇 JUnit 4,會自動創(chuàng)建 MainActivityTest 至之前修改的 test 目錄下

- 編寫 Test Case,這里直接貼上測試代碼,代碼都相當(dāng)簡單
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默認(rèn)顯示的內(nèi)容
assertEquals("Hello world!", textView.getText().toString());
}
@Test
public void testButton() throws Exception{
//點(diǎn)擊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 面板,在面板中執(zhí)行測試

- 右鍵 MainActivityTest > Run 'MainActivityTest'
- 在終端中運(yùn)行 ./gradlew test
查看報(bào)告
執(zhí)行完測試之后,會在 app/build/reports/tests/目錄下生成相應(yīng)地測試報(bào)告,使用瀏覽器打開

Assert
- Junit3和Junit4都提供了一個Assert類(雖然package不同,但是大致差不多)。Assert類中定義了很多靜態(tài)方法來進(jìn)行斷言。列表如下:
- assertTrue(String message, boolean condition) 要求condition == true
- assertFalse(String message, boolean condition) 要求condition == false
- fail(String message) 必然失敗,同樣要求代碼不可達(dá)
- 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測重于對功能的測試重現(xiàn)。比如對于List接口,Mock會直接對List進(jìn)行模擬,而Stub會新建一個實(shí)現(xiàn)了List的TestList,在其中編寫測試的代碼。
強(qiáng)烈建議優(yōu)先選擇Mock方式,因?yàn)镸ock方式下,模擬代碼與測試代碼放在一起,易讀性好,而且擴(kuò)展性、靈活性都比Stub好。
比較流行的Mock有:
其中EasyMock和Mockito對于Java接口使用接口代理的方式來模擬,對于Java類使用繼承的方式來模擬(也即會創(chuàng)建一個新的Class類)。Mockito支持spy方式,可以對實(shí)例進(jìn)行模擬。但它們都不能對靜態(tài)方法和final類進(jìn)行模擬,powermock通過修改字節(jié)碼來支持了此功能。
使用Mockito進(jìn)行單元測試
介紹
Mockito是Google Code上的一個開源項(xiàng)目,Api相對于EasyMock更好友好。與EasyMock不同的是,Mockito沒有錄制過程,只需要在“運(yùn)行測試代碼”之前對接口進(jìn)行Stub,也即設(shè)置方法的返回值或拋出的異常,然后直接運(yùn)行測試代碼,運(yùn)行期間調(diào)用Mock的方法,會返回預(yù)先設(shè)置的返回值或拋出異常,最后再對測試代碼進(jìn)行驗(yàn)證??梢圆榭?a target="_blank" rel="nofollow">此文章了解兩者的不同。
官方提供了很多樣例,基本上包括了所有功能,可以去看看。
引入方法
在你的Gradle文件中加入下面的依賴:
repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:1.9.5" }
示例
這里從官方樣例中摘錄幾個典型的:
- 驗(yàn)證調(diào)用行為
import static org.mockito.Mockito.*;
//創(chuàng)建Mock
List mockedList = mock(List.class);
//使用Mock對象
mockedList.add("one");
mockedList.clear();
//驗(yàn)證行為
verify(mockedList).add("one");
verify(mockedList).clear();
- 對Mock對象進(jìn)行Stub
//也可以Mock具體的類,而不僅僅是接口
LinkedList mockedList = mock(LinkedList.class);
//Stub
when(mockedList.get(0)).thenReturn("first"); // 設(shè)置返回值
when(mockedList.get(1)).thenThrow(new RuntimeException()); // 拋出異常
//第一個會打印 "first"
System.out.println(mockedList.get(0));
//接下來會拋出runtime異常
System.out.println(mockedList.get(1));
//接下來會打印"null",這是因?yàn)闆]有stub get(999)
System.out.println(mockedList.get(999));
// 可以選擇性地驗(yàn)證行為,比如只關(guān)心是否調(diào)用過get(0),而不關(guān)心是否調(diào)用過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 >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模擬服務(wù)端
介紹
我們的Android應(yīng)用程序經(jīng)常要從后端獲取數(shù)據(jù)來進(jìn)行相關(guān)交互,但前端和后臺的開發(fā)往往是分開進(jìn)行的,如果后臺沒有開發(fā)完成,前端甚至都無法進(jìn)行調(diào)試,這是很浪費(fèi)時(shí)間的。我們這里引入一個Mockwebserver的庫,他可以模擬一個服務(wù),對HTTP和HTTPS的請求返回指定的數(shù)據(jù),從而用來驗(yàn)證我們的應(yīng)用程序是否達(dá)到預(yù)期效果。你可以確定你正在進(jìn)行的測試都走的是完整的HTTP協(xié)議棧。你甚至可以從真正的web服務(wù)器復(fù)制HTTP響應(yīng)來創(chuàng)建你的測試案例。甚至,你還可以在代碼中生成比較難以重現(xiàn)的類似500的錯誤或緩慢的加載響應(yīng)的情況。詳細(xì)內(nèi)容可以參考Github上mockwebserver的介紹。
使用方法
我們可以像使用Mockito一樣來使用MockWebServer,使用步驟如下:
- 在你的gradle文件中加入
testCompile 'com.squareup.okhttp:mockwebserver:2.4.0'
- 設(shè)計(jì)Mock腳本
- 運(yùn)行你的應(yīng)用程序
- 驗(yàn)證返回結(jié)果
示例
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默認(rèn)返回一個空的response body和一個200的狀態(tài)碼,你可以自定義body的內(nèi)容(可以是字符串,數(shù)組,json等),你還可以通過fluent builder API來對你的響應(yīng)添加headers
MockResponse response = new MockResponse()
.addHeader("Content-Type", "application/json; charset=utf-8")
.addHeader("Cache-Control", "no-cache")
.setBody("{}");
MockResponse還可以用來模擬慢速網(wǎng)絡(luò),這樣你能通過設(shè)置延遲來測試超時(shí)或者弱網(wǎng)
response.throttleBody(1024, 1, TimeUnit.SECONDS);
RecordedRequest
我們可以通過RecordedRequest來檢查發(fā)送過來的請求的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
默認(rèn)情況下,MockWebServer使用隊(duì)列的方式來處理請求,但我們還有另外一種方式來處理請求,就是使用Dispatcher,它根據(jù)請求路徑來過濾并分發(fā)響應(yīng)結(jié)果。
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進(jìn)行數(shù)據(jù)庫單元測試
簡介
DbUnit 是專門針對數(shù)據(jù)庫測試的對JUnit的一個擴(kuò)展,它可以將測試對象數(shù)據(jù)庫置于一個測試輪回之間的狀態(tài)。熟悉單元測試的開發(fā)人員都知道,在對數(shù)據(jù)庫進(jìn)行單元測試時(shí)候,通常采用的方案有運(yùn)用模擬對象(mock objects) 和stubs 兩種。通過隔離關(guān)聯(lián)的數(shù)據(jù)庫訪問類,比如JDBC 的相關(guān)操作類,來達(dá)到對數(shù)據(jù)庫操作的模擬測試。然而某些特殊的系統(tǒng),比如利用了EJB 的CMP(container-managed persistence) 的系統(tǒng),數(shù)據(jù)庫的訪問對象是在最底層而且很隱蔽的,那么這兩種解決方案對這些系統(tǒng)就顯得力不從心了。
DBUnit的設(shè)計(jì)理念就是在測試之前,備份數(shù)據(jù)庫,然后給對象數(shù)據(jù)庫植入我們需要的準(zhǔn)備數(shù)據(jù),最后,在測試完畢后,讀入備份數(shù)據(jù)庫,回溯到測試前的狀態(tài);而且又因?yàn)镈BUnit 是對JUnit 的一種擴(kuò)展,開發(fā)人員可以通過創(chuàng)建測試用例代碼,在這些測試用例的生命周期內(nèi)來對數(shù)據(jù)庫的操作結(jié)果進(jìn)行比較。
DbUnit 測試基本概念和流程
基于DbUnit 的測試的主要接口是IDataSet 。IDataSet 代表一個或多個表的數(shù)據(jù)。
可以將數(shù)據(jù)庫模式的全部內(nèi)容表示為單個IDataSet 實(shí)例。這些表本身由Itable 實(shí)例來表示。
IDataSet 的實(shí)現(xiàn)有很多,每一個都對應(yīng)一個不同的數(shù)據(jù)源或加載機(jī)制。最常用的幾種 IDataSet 實(shí)現(xiàn)為:
- FlatXmlDataSet :數(shù)據(jù)的簡單平面文件 XML 表示
- QueryDataSet :用 SQL 查詢獲得的數(shù)據(jù)
- DatabaseDataSet :數(shù)據(jù)庫表本身內(nèi)容的一種表示
- XlsDataSet :數(shù)據(jù)的excel 表示
一般而言,使用DbUnit 進(jìn)行單元測試的流程如下: - 根據(jù)業(yè)務(wù),做好測試用的準(zhǔn)備數(shù)據(jù)和預(yù)想結(jié)果數(shù)據(jù),通常準(zhǔn)備成xml 格式文件。
- 在setUp() 方法里邊備份數(shù)據(jù)庫中的關(guān)聯(lián)表。
- 在setUp() 方法里邊讀入準(zhǔn)備數(shù)據(jù)。
- 對測試類的對應(yīng)測試方法進(jìn)行實(shí)裝: 執(zhí)行對象方法,把數(shù)據(jù)庫的實(shí)際執(zhí)行結(jié)果和預(yù)想結(jié)果進(jìn)行比較。
- 在tearDown() 方法里邊, 把數(shù)據(jù)庫還原到測試前狀態(tài)。
DbUnit 開發(fā)實(shí)例
下面通過一個實(shí)例來說明DbUnit 的實(shí)際運(yùn)用。
比如有一個學(xué)生表[student] ,結(jié)構(gòu)如下:
id char(4) pk 學(xué)號
name char(50) 姓名
sex char(1) 性別
birthday date 出生日期
1 準(zhǔn)備數(shù)據(jù)如下:
| id | name | sex | birthday |
|---|---|---|---|
| 0001 | 翁仔 | m | 1979-12-31 |
| 0002 | 王翠花 | f | 1982-08-09 |
測試對象類為StudentOpe.java ,里邊有2 個方法:
findStudent(String id) : 根據(jù)主鍵id 找記錄
addStudent(Student student) :添加一條記錄
在測試addStudent 方法時(shí)候,我們準(zhǔn)備添加如下一條數(shù)據(jù)
| id | name | sex | birthday |
|---|---|---|---|
| 0088 | 王耳朵 | m | 1982-01-01 |
那么在執(zhí)行該方法后,數(shù)據(jù)庫的student 表里的數(shù)據(jù)是這樣的:
| id | name | sex | birthday |
|---|---|---|---|
| 0001 | 翁仔 | m | 1979-12-31 |
| 0002 | 王翠花 | f | 1982-08-09 |
| 0088 | 王耳朵 | m | 1982-01-01 |
然后我們說明如何對這2 個方法進(jìn)行單元測試。
實(shí)例展開
1 把準(zhǔn)備數(shù)據(jù)和預(yù)想數(shù)據(jù)轉(zhuǎn)換成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 實(shí)裝setUp 方法,詳細(xì)見代碼注釋。
protected void setUp() {
IDatabaseConnection connection =null;
try{
super.setUp();
// 本例使用postgresql 數(shù)據(jù)庫
Class.forName("org.postgresql.Driver");
// 連接DB
Connection conn=DriverManager.getConnection("jdbc:postgresql:testdb.test","postgres","postgres");
// 獲得DB 連接
connection =new DatabaseConnection(conn);
// 對數(shù)據(jù)庫中的操作對象表student 進(jìn)行備份
QueryDataSet backupDataSet = new QueryDataSet(connection);
backupDataSet.addTable("student");
file=File.createTempFile("student_back",".xml");// 備份文件
FlatXmlDataSet.write(backupDataSet,new FileOutputStream(file));
// 準(zhǔn)備數(shù)據(jù)的讀入
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 實(shí)裝測試方法,詳細(xì)見代碼注釋。
- 檢索類方法,可以利用assertEquals() 方法,拿表的字段進(jìn)行比較。
// findStudent
public void testFindStudent() throws Exception{
// 執(zhí)行findStudent 方法
StudentOpe studentOpe=new StudentOpe();
Student result = studentOpe.findStudent("0001");
// 預(yù)想結(jié)果和實(shí)際結(jié)果的比較
assertEquals(" 翁仔",result.getName());
assertEquals("m",result.getSex());
assertEquals("1979-12-31",result.getBirthDay());
}
- 更新,添加,刪除等方法,可以利用Assertion.assertEquals() 方法,拿表的整體來比較。
public void testAddStudent() throws Exception{
// 執(zhí)行addStudent 方法
StudentOpe studentOpe=new StudentOpe();
// 被追加的記錄
Student newStudent = new Student("0088"," 王耳朵","m","1982-01-01");
// 執(zhí)行追加方法
Student result = studentOpe.addStudent(newStudent);
// 預(yù)想結(jié)果和實(shí)際結(jié)果的比較
IDatabaseConnection connection=null;
try{
// 預(yù)期結(jié)果取得
IDataSet expectedDataSet = new FlatXmlDataSet(new FileInputStream("student_exp.xml"));
ITable expectedTable = expectedDataSet.getTable("student");
// 實(shí)際結(jié)果取得
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();
}
}
- 如果在整體比較表的時(shí)候,有個別字段不需要比較,可以用DefaultColumnFilter.excludedColumnsTable() 方法,
將指定字段給排除在比較范圍之外。比如上例中不需要比較birthday 這個字段的話,那么可以如下代碼所示進(jìn)行處理:
ITable filteredExpectedTable = DefaultColumnFilter.excludedColumnsTable(expectedTable, new String[]{"birthday"});
ITable filteredActualTable = DefaultColumnFilter.excludedColumnsTable(actualTable,new String[]{"birthday"});
Assertion.assertEquals(filteredExpectedTable, filteredActualTable);
4 在tearDown() 方法里邊, 把數(shù)據(jù)庫還原到測試前狀態(tài)
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){}
}
}
關(guān)于Android-async-http的單元測試
如果我們直接在AndroidTestCase中對異步請求的方法進(jìn)行測試,會發(fā)現(xiàn)根本沒有返回結(jié)果,測試就結(jié)束了。這是因?yàn)锳ndroid的單元測試根本不是在主線程跑的,但我們的異步請求創(chuàng)建的Handler并不是綁定到主線程,而是綁定到創(chuàng)建它的線程,即測試線程。這樣,測試一結(jié)束,Handler也就釋放,異步返回就的消息就找不到Handler了。為了解決這個問題,我們發(fā)現(xiàn) ActivityTestCase 擁有一個可以在主線程運(yùn)行的測試API:runTestOnUiThread,只要將測試代碼放進(jìn)去即可,示例如下:
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");
}
}
代碼覆蓋率
代碼覆蓋率的作用主要是用來查看執(zhí)行完畢后,有哪些代碼尚未覆蓋到,未覆蓋到的代碼通常意味著未覆蓋到的功能或場景,目前主流的Android覆蓋率工具有開源軟件Emma和Jacoco。
Emma
- 第一步:把被測工程生成Ant build文件,andriod-app就是工程名
android update project -p android-app - 第二步:將andriod測試工程也轉(zhuǎn)換成ant工程,-m選項(xiàng)指定了測試工程對應(yīng)的主andriod工程的位置,而android-test就是測試工程名:
android update test-project -m ../android-app -p android-test
- 第三步:執(zhí)行
下面的命令,編譯、執(zhí)行單元測試、收集覆蓋率:
ant clean emma debug install
Jacoco
JaCoCo(Java Code Coverage)是一種分析單元測試覆蓋率的工具,使用它運(yùn)行單元測試后,可以給出代碼中哪些部分被單元測試測到,哪些部分沒有沒測到,并且給出整個項(xiàng)目的單元測試覆蓋情況百分比,看上去一目了然。下面介紹一下如何在Android studio中配置Jacoco為單元測試執(zhí)行覆蓋率
在Gradle中加入Jacoco
在build.gradle文件中加入下面的配置項(xiàng)
apply plugin: 'jacoco'
jacoco{
toolVersion = "0.7.5.201505241946"
}
.....
buildTypes {
debug {
testCoverageEnabled = true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
執(zhí)行Jacoco
先執(zhí)行單元測試用例,然后執(zhí)行Jacoco
./gradlew clean createDebugCoverageReport
查看覆蓋率結(jié)果


總結(jié)
關(guān)于Android單元測試,個人還是比較推薦Robolectric+Mockito的組合方案。但技術(shù)和框架都只是一方面,真正需要推動的是培養(yǎng)開發(fā)人員單元測試的意識。對于一個單元測試做得足夠好的項(xiàng)目,是不需要擔(dān)心質(zhì)量問題的,測試人員應(yīng)該只需要做質(zhì)量驗(yàn)收即可。