前言
本文將介紹在Android Studio中,android單元測試的介紹和實(shí)現(xiàn)。相關(guān)代碼托管在github上的AndroidJunitDemo中凛驮,涉及到的用例代碼收集于google官方提供的測試用例android-testing棒假,同時(shí)進(jìn)行了簡化和修改。你可以從該demo中學(xué)習(xí)單元測試簡單的使用烂瘫,在工程中媒熊,包含兩個(gè)模塊,一個(gè)實(shí)現(xiàn)計(jì)算器功能的CalculationActivity坟比,另外一個(gè)是PersonlInfoActivity芦鳍,可以編輯姓名,郵箱和生日等信息葛账,并保存到SharePreferences中柠衅,同時(shí)提供了兩個(gè)模塊的單元測試。
單元測試
關(guān)于單元測試籍琳,在維基百科中茄茁,給出了如下定義:
在計(jì)算機(jī)編程中,單元測試(英語:Unit Testing)又稱為模塊測試, 是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測試工作巩割。程序單元是應(yīng)用的最小可測試部件裙顽。在過程化編程中,一個(gè)單元就是單個(gè)程序宣谈、函數(shù)愈犹、過程等;對(duì)于面向?qū)ο缶幊蹋钚卧褪欠椒ㄤ鲈酰ɑ悾ǔ悾┭薄⒊橄箢悺⒒蛘吲缮悾ㄗ宇悾┲械姆椒ā?/p>
android中的單元測試基于JUnit勋锤,可分為本地測試和instrumented測試饭玲,在項(xiàng)目中對(duì)應(yīng)
- module-name/src/test/java/.
該目錄下的代碼運(yùn)行在本地JVM上,其優(yōu)點(diǎn)是速度快叁执,不需要設(shè)備或模擬器的支持茄厘,但是無法直接運(yùn)行含有android系統(tǒng)API引用的測試代碼。 - module-name/src/androidTest/java/.
該目錄下的測試代碼需要運(yùn)行在android設(shè)備或模擬器下面谈宛,因此可以使用android系統(tǒng)的API次哈,速度較慢。
以上分別執(zhí)行在JUnit和AndroidJUnitRunner的測試運(yùn)行環(huán)境吆录,兩者主要的區(qū)別在于是否需要android系統(tǒng)API的依賴窑滞。
在實(shí)際開發(fā)過程中,我們應(yīng)該盡量用JUnit實(shí)現(xiàn)本地JVM的單元測試恢筝,而項(xiàng)目中的代碼大致可分為以下三類:
- 1.強(qiáng)依賴關(guān)系哀卫,如在Activity,Service等組件中的方法撬槽,其特點(diǎn)是大部分為private方法此改,并且與其生命周期相關(guān),無法直接進(jìn)行單元測試恢氯,可以進(jìn)行Ecspreso等UI測試带斑。
- 2.部分依賴,代碼實(shí)現(xiàn)依賴注入勋拟,該類需要依賴Context等android對(duì)象的依賴勋磕,可以通過Mock或其它第三方框架實(shí)現(xiàn)JUnit單元測試或使用androidJunitRunner進(jìn)行單元測試。
- 3.純java代碼敢靡,不存在對(duì)android庫的依賴挂滓,可以進(jìn)行JUnit單元測試
常用的測試框架
在android測試框架中,常用的有以下幾個(gè)框架和工具類:
- JUnit4
- AndroidJUnitRunner
- Mockito
- Espresso
關(guān)于單元測試框架的選擇啸胧,可以參考下圖:
JUnit4
JUnit4是一套基于注解的單元測試框架赶站。在android studio中,編寫在test目錄下的測試類都是基于該框架實(shí)現(xiàn)纺念,該目錄下的測試代碼運(yùn)行在本地的JVM上贝椿,不需要設(shè)備(真機(jī)或模擬器)的支持。
JUnit4中常用的幾個(gè)注解:
- @BeforeClass 測試類里所有用例運(yùn)行之前陷谱,運(yùn)行一次這個(gè)方法烙博。方法必須是public static void
- @AfterClass 與BeforeClass對(duì)應(yīng)
- @Before 在每個(gè)用測試?yán)\(yùn)行之前都運(yùn)行一次瑟蜈。
- @After 與Before對(duì)應(yīng)
- @Test 指定該方法為測試方法,方法必須是public void
- @RunWith 測試類名之前渣窜,用來確定這個(gè)類的測試運(yùn)行器
對(duì)于其它的注解铺根,可以通過查看junit4官網(wǎng)來進(jìn)一步學(xué)習(xí)。
在test下添加測試類乔宿,對(duì)于需要進(jìn)行測試的方法添加@Test注解位迂,在該方法中使用assert進(jìn)行判斷,為了使assert更加直觀详瑞,方便掂林,可以使用Hamcrest library,通過使用hamcrest的匹配工具蛤虐,可以讓你更靈活的進(jìn)行測試党饮。 以下是一個(gè)最簡單的測試類CalculatorTest的實(shí)現(xiàn):
public class CalculatorTest {
/** 計(jì)算功能類 */
private Calculator mCalculator;
@Before
public void setUp() {
mCalculator = new Calculator();
}
/**
* 方法的命名盡量描述詳細(xì)
* 測試兩個(gè)數(shù)相加
*/
@Test
public void addTwoNumbers() {
double resultAdd = mCalculator.add(1d, 1d);
//使用hamcrest進(jìn)行assert肝陪,直觀驳庭,易讀
assertThat(resultAdd, is(equalTo(2d)));
}
……
}
當(dāng)需要傳入多個(gè)參數(shù)進(jìn)行條件,即條件覆蓋時(shí)氯窍,可以使用@Parameters來進(jìn)行單個(gè)方法的多次不同參數(shù)的測試饲常,對(duì)應(yīng)Demo中的CalculatorWithParameterizedTest測試類,使用該方法需要如下步驟:
- 1.在測試類上添加@RunWith(Parameterized.class)注解狼讨。
- 2.添加構(gòu)造方法贝淤,并將測試的參數(shù)作為其構(gòu)造參數(shù)。
- 3.添加獲取參數(shù)集合的static方法政供,并在該方法上添加@Parameters注解播聪。
- 4.在需要測試的方法中直接使用成員變量,該變量由JUnit通過構(gòu)造方法生成布隔。
@RunWith(Parameterized.class)
public class CalculatorWithParameterizedTest {
/** 參數(shù)的變量 */
private final double mOperandOne;
private final double mOperandTwo;
/** 期待值 */
private final double mExpectedResult;
/** 計(jì)算類 */
private Calculator mCalculator;
/**
* 構(gòu)造方法离陶,框架可以自動(dòng)填充參數(shù)
*/
public CalculatorWithParameterizedTest(double operandOne, double operandTwo,
double expectedResult){
mOperandOne = operandOne;
mOperandTwo = operandTwo;
mExpectedResult = expectedResult;
}
/**
* 需要測試的參數(shù)和對(duì)應(yīng)結(jié)果
*/
@Parameterized.Parameters
public static Collection<Object[]> initData(){
return Arrays.asList(new Object[][]{
{0, 0, 0},
{0, -1, -1},
{2, 2, 4},
{8, 8, 16},
{16, 16, 32},
{32, 0, 32},
{64, 64, 128}});
}
@Before
public void setUp() {
mCalculator = new Calculator();
}
/**
* 使用參數(shù)組測試加的相關(guān)操作
*/
@Test
public void testAdd_TwoNumbers() {
double resultAdd = mCalculator.add(mOperandOne, mOperandTwo);
assertThat(resultAdd, is(equalTo(mExpectedResult)));
}
}
現(xiàn)在目錄下存在如下兩個(gè)Test類:
如果我們需要同時(shí)運(yùn)行兩個(gè)或多個(gè)Test類怎么辦?JUnit提供了Suite注解衅檀,在對(duì)應(yīng)的測試目錄下創(chuàng)建一個(gè)空Test類招刨,如Demo里的UnitTestSuite,該類上添加如下注解:
- @RunWith(Suite.class):配置Runner運(yùn)行環(huán)境哀军。
- @Suite.SuiteClasses({A.class, B.class}):添加需要一起運(yùn)行的測試類沉眶。
@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class, CalculatorWithParameterizedTest.class})
public class UnitTestSuite {
}
目前為止已經(jīng)可以完成簡單的單元測試了,但在android中杉适,方法中使用到android系統(tǒng)api是一件司空見慣的事谎倔,比如Context,Parcelable猿推,SharedPreferences等等片习。而在本地JVM中無法調(diào)用這些接口,因此,我們就需要使用AndroidJUnitRunner來完成這些方法的測試
AndroidJUnitRunner
當(dāng)單元測試中涉及到大量的android系統(tǒng)庫的調(diào)用時(shí)毯侦,你可以通過該方案類完成測試哭靖。使用方法是在androidTest目錄下創(chuàng)建測試類,在該類上添加@RunWith(AndroidJUnit4.class)注解侈离。
在Demo中androidTest目錄下的SharedPreferencesHelperTest測試類试幽,該類對(duì)SharedPreferencesHelper進(jìn)行了單元測試,其方法內(nèi)部涉及到了SharedPreferences卦碾,該類屬于android系統(tǒng)的api铺坞,因此無法直接在test中運(yùn)行。部分實(shí)現(xiàn)代碼如下:
@RunWith(AndroidJUnit4.class)
public class SharedPreferencesHelperTest {
private static final String TEST_NAME = "Test name";
private static final String TEST_EMAIL = "test@email.com";
private static final Calendar TEST_DATE_OF_BIRTH = Calendar.getInstance();
private SharedPreferenceEntry mSharedPreferenceEntry;
private SharedPreferencesHelper mSharedPreferencesHelper;
private SharedPreferences mSharePreferences;
/** 上下文 */
private Context mContext;
……
@Before
public void setUp() throws Exception {
//獲取application的context
mContext = InstrumentationRegistry.getTargetContext();
//實(shí)例化SharedPreferences
mSharePreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
mSharedPreferenceEntry = new SharedPreferenceEntry(TEST_NAME, TEST_DATE_OF_BIRTH, TEST_EMAIL);
//實(shí)例化SharedPreferencesHelper洲胖,依賴注入SharePreferences
mSharedPreferencesHelper = new SharedPreferencesHelper(mSharePreferences);
//以下是在mock的相關(guān)操作济榨,模擬commit失敗
mMockSharePreferences = Mockito.mock(SharedPreferences.class);
mMockBrokenEditor = Mockito.mock(SharedPreferences.Editor.class);
when(mMockSharePreferences.edit()).thenReturn(mMockBrokenEditor);
when(mMockBrokenEditor.commit()).thenReturn(false);
mMockSharedPreferencesHelper = new SharedPreferencesHelper(mMockSharePreferences);
}
/**
* 測試保存數(shù)據(jù)是否成功
*/
@Test
public void sharedPreferencesHelper_SavePersonalInformation() throws Exception {
assertThat(mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry), is(true));
}
/**
* 測試保存數(shù)據(jù),然后獲取數(shù)據(jù)是否成功
*/
@Test
public void sharedPreferencesHelper_SaveAndReadPersonalInformation() throws Exception {
mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry);
SharedPreferenceEntry sharedPreferenceEntry = mSharedPreferencesHelper.getPersonalInfo();
assertThat(isEquals(mSharedPreferenceEntry, sharedPreferenceEntry), is(true));
}
……
}
在AndroidJUnitRunner中绿映,通過InstrumentationRegistry來獲取Context擒滑,并實(shí)例化SharedPreferences,然后通過依賴注入來完成SharedPreferencesHelper對(duì)象的生成叉弦。對(duì)于AndroidJUnitRunner更詳細(xì)的介紹丐一,可以參考android官方文檔測試支持庫。
使用AndroidJUnitRunner最大的缺點(diǎn)在于無法在本地JVM運(yùn)行淹冰,直接的結(jié)果就是測試速度慢库车,同時(shí)無法執(zhí)行覆蓋測試。因此出現(xiàn)了很多替代方案樱拴,比如在設(shè)計(jì)合理柠衍,依賴注入實(shí)現(xiàn)的代碼,可以使用Mockito來進(jìn)行本地測試晶乔,或者使用第三方測試框架Robolectric等珍坊。
Mockito
涉及到android依賴的方法的測試,除了在androidTest使用瘪弓,還可以通過mock來執(zhí)行本地測試垫蛆。使用Mock的目的主要有以下兩點(diǎn):
- 驗(yàn)證這個(gè)對(duì)象的某些方法的調(diào)用情況,調(diào)用了多少次腺怯,參數(shù)是什么等等
- 指定這個(gè)對(duì)象的某些方法的行為袱饭,返回特定的值,或者是執(zhí)行特定的動(dòng)作
Mockito是優(yōu)秀的mock框架之一呛占,使用該框架可以使mock的操作更加簡單虑乖,直觀。
要使用Mockito晾虑,需要添加如下依賴:
dependencies {
testCompile 'junit:junit:4.12'
// 如果要使用Mockito疹味,你需要添加此條依賴庫
testCompile 'org.mockito:mockito-core:1.+'
// 如果你要使用Mockito 用于 Android instrumentation tests仅叫,那么需要你添加以下三條依賴庫
androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
}
在AndroidJUnitRunner介紹中的對(duì)于SharedPreferencesHelper的測試,由于其依賴注入的設(shè)計(jì)糙捺,我們可以方便的去mock一個(gè)SharePreferences來執(zhí)行本地的測試诫咱。在Demo中的test目錄下的SharedPreferencesHelperWithMockTest類即通過mockito來完成測試的,主要代碼如下:
@RunWith(MockitoJUnitRunner.class)
public class SharedPreferencesHelperWithMockTest {
private static final String TEST_NAME = "Test name";
private static final String TEST_EMAIL = "test@email.com";
private static final Calendar TEST_DATE_OF_BIRTH = Calendar.getInstance();
private SharedPreferencesHelper mSharedPreferencesHelper;
private SharedPreferenceEntry mSharedPreferenceEntry;
……
@Mock
SharedPreferences mMockSharedPreferences;
@Mock
SharedPreferences.Editor mMockEditor;
……
@Before
public void setUp() throws Exception {
mSharedPreferenceEntry = new SharedPreferenceEntry(TEST_NAME, TEST_DATE_OF_BIRTH, TEST_EMAIL);
mSharedPreferencesHelper = new SharedPreferencesHelper(mockSharePreferences());
……
}
@Test
public void sharedPreferencesHelper_SavePersonalInformation() throws Exception {
assertThat(mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry), is(true));
}
@Test
public void sharedPreferencesHelper_SaveAndReadPersonalInformation() throws Exception {
mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry);
SharedPreferenceEntry sharedPreferenceEntry = mSharedPreferencesHelper.getPersonalInfo();
assertThat(isEquals(mSharedPreferenceEntry, sharedPreferenceEntry), is(true));
}
……
/**
* 編寫Mock相關(guān)代碼洪灯,代碼中mock了SharedPreferences類的getXxx的相關(guān)操作坎缭,
* 均返回SharedPreferenceEntry對(duì)象的值,同時(shí)在代碼中使用到了commit和edit签钩,都需要在方法中進(jìn)行mock實(shí)現(xiàn)
* Creates a mocked SharedPreferences.
*/
private SharedPreferences mockSharePreferences(){
when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_NAME), anyString()))
.thenReturn(mSharedPreferenceEntry.getName());
when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_EMAIL), anyString()))
.thenReturn(mSharedPreferenceEntry.getEmail());
when(mMockSharedPreferences.getLong(eq(SharedPreferencesHelper.KEY_DOB), anyLong()))
.thenReturn(mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis());
when(mMockEditor.commit()).thenReturn(true);
when(mMockSharedPreferences.edit()).thenReturn(mMockEditor);
return mMockSharedPreferences;
}
……
}
Espresso
在Demo中掏呼,除了單元測試的用例,還提供了一個(gè)CalculatorInstrumentationTest測試類铅檩,該類使用Espresso憎夷,一個(gè)官方提供了UI測試框架。注意昧旨,UI測試不屬于單元測試的范疇拾给。通過Espresso的使用,可以編寫簡潔臼予、運(yùn)行可靠的自動(dòng)化UI測試鸣戴。詳細(xì)的使用可以參考測試支持庫中關(guān)于Espresso的使用介紹啃沪。
@RunWith(AndroidJUnit4.class)
@LargeTest
public class CalculatorInstrumentationTest {
/**
* 在測試中運(yùn)行Activity
* A JUnit {@link Rule @Rule} to launch your activity under test. This is a replacement
* for {@link ActivityInstrumentationTestCase2}.
* <p>
* Rules are interceptors which are executed for each test method and will run before
* any of your setup code in the {@link Before @Before} method.
* <p>
* {@link ActivityTestRule} will create and launch of the activity for you and also expose
* the activity under test. To get a reference to the activity you can use
* the {@link ActivityTestRule#getActivity()} method.
*/
@Rule
public ActivityTestRule<CalculatorActivity> mActivityRule = new ActivityTestRule<>(
CalculatorActivity.class);
……
private void performOperation(int btnOperationResId, String operandOne,
String operandTwo, String expectedResult) {
// 指定輸入框中輸入文本粘拾,同時(shí)關(guān)閉鍵盤
onView(withId(R.id.operand_one_edit_text)).perform(typeText(operandOne),
closeSoftKeyboard());
onView(withId(R.id.operand_two_edit_text)).perform(typeText(operandTwo),
closeSoftKeyboard());
// 獲取特定按鈕執(zhí)行點(diǎn)擊事件
onView(withId(btnOperationResId)).perform(click());
// 獲取文本框中顯示的結(jié)果
onView(withId(R.id.operation_result_text_view)).check(matches(withText(expectedResult)));
}
}
你可以運(yùn)行CalculatorInstrumentationTest測試類,會(huì)有一個(gè)直觀的認(rèn)識(shí)创千。
運(yùn)行單元測試
在Android Studio中缰雇,可以通過以下兩種方式運(yùn)行單元測試:
- 手動(dòng)運(yùn)行
- 通過指令運(yùn)行
1.手動(dòng)運(yùn)行
在Android Studio中,對(duì)指定的測試類右鍵追驴,選擇對(duì)應(yīng)的RUN或DEBUG操作選項(xiàng)即可運(yùn)行械哟,如下圖:
圖中第三個(gè)為覆蓋測試,即運(yùn)行所有的test下的單元測試殿雪,并顯示單元測試的覆蓋率暇咆。如果需要保存測試結(jié)果,可以在結(jié)果框中點(diǎn)擊Export Test Results按鈕:
結(jié)果會(huì)被保存到項(xiàng)目的目錄下丙曙,可以通過瀏覽器打開查看:
2.指令運(yùn)行
在Terminal輸入gradle testDebugUnitTest或gradle testReleaseUnitTest指令來分別運(yùn)行debug和release版本的unit testing爸业,在執(zhí)行的結(jié)果可以在xxx\project\app\build\reports\tests\testReleaseUnitTest中查看:
其它
關(guān)于異步操作的單元測試
在實(shí)際的android開發(fā)過程中,經(jīng)常涉及到異步操作亏镰,比如網(wǎng)絡(luò)請(qǐng)求扯旷,Rxjava的線程調(diào)度等。在單元測試中索抓,往往測試方法執(zhí)行往了钧忽,異步操作還沒介紹毯炮,這就導(dǎo)致了無法順利的執(zhí)行單元測試操作。其解決方法可以提供CountDownLatch類來阻塞測試方法的線程耸黑,當(dāng)異步操作完成后(通過回調(diào))來喚醒繼續(xù)執(zhí)行測試桃煎,獲取結(jié)果。其實(shí)對(duì)于網(wǎng)絡(luò)請(qǐng)求這種操作應(yīng)該使用Mock來替代大刊,因?yàn)槟愕膯卧獪y試的結(jié)果不應(yīng)受網(wǎng)絡(luò)的影響备禀,不需要關(guān)注網(wǎng)絡(luò)是否正常,服務(wù)器是否崩潰奈揍,而應(yīng)該把關(guān)注點(diǎn)放在單元本身的操作曲尸。
單元測試,集成測試男翰,UI測試
- UI測試是測試到交互和視覺俊马,以及操作的結(jié)果是否符合預(yù)期〈兴可以通過Espresso撞反,UI Automator等框架,或者人工測試租冠。
- 集成測試是基于單元測試鹏倘,將多個(gè)單元測試組裝起來進(jìn)行測試,實(shí)際測試往往會(huì)運(yùn)行慢顽爹,依賴過多導(dǎo)致集成測試非常費(fèi)時(shí)纤泵。
- 單元測試僅針對(duì)最小單元,在面向?qū)ο笾芯翟粒瑔卧傅氖欠椒筇猓ɑ悾ǔ悾⒊橄箢惾饪省⒒蛘吲缮悾ㄗ宇悾┲械姆椒ā?/li>
三者的在實(shí)際應(yīng)用中可以通過Test Pyramid(Martin Fowler的總結(jié))來衡量:
所以對(duì)于測試公荧,在開放過程中,我們(開發(fā)者)需要把更多的精力放在單元測試上同规。
擴(kuò)展閱讀
- android關(guān)于測試的官方文檔Testing Apps on Android
- 對(duì)于UI測試循狰,google還提供了一個(gè)UI Automator測試框架
- 關(guān)于單元測試,小創(chuàng)作 的系列文章可以幫助你更好的學(xué)習(xí)和使用相關(guān)技術(shù)
- Android單元測試 - 如何開始券勺?該文簡單的介紹了單元測試相關(guān)的幾個(gè)概念绪钥。