android中的單元測試基于JUnit,可分為本地測試和instrumented測試,在項目中對應(yīng)
module-name/src/test/java/.
該目錄下的代碼運行在本地JVM上,其優(yōu)點是速度快,不需要設(shè)備或模擬器的支持,但是無法直接運行含有android系統(tǒng)API引用的測試代碼。-
module-name/src/androidTest/java/.
該目錄下的測試代碼需要運行在android設(shè)備或模擬器下面,因此可以使用android系統(tǒng)的API,速度較慢。
image-20200613151726348.png
以上分別執(zhí)行在JUnit和AndroidJUnitRunner的測試運行環(huán)境,兩者主要的區(qū)別在于是否需要android系統(tǒng)API的依賴。
在實際開發(fā)過程中,我們應(yīng)該盡量用JUnit實現(xiàn)本地JVM的單元測試,而項目中的代碼大致可分為以下三類:
- 1.強依賴關(guān)系,如在Activity,Service等組件中的方法,其特點是大部分為private方法,并且與其生命周期相關(guān),無法直接進行單元測試,可以進行Ecspreso等UI測試。
- 2.部分依賴,代碼實現(xiàn)依賴注入,該類需要依賴Context等android對象的依賴,可以通過Mock或其它第三方框架實現(xiàn)JUnit單元測試或使用androidJunitRunner進行單元測試。
- 3.純java代碼,不存在對android庫的依賴,可以進行JUnit單元測試
常用的測試框架
在android測試框架中,常用的有以下幾個框架和工具類:
- JUnit4
- AndroidJUnitRunner
- Mockito
- Espresso
關(guān)于單元測試框架的選擇,可以參考下圖:

JUnit4
JUnit4是一套基于注解的單元測試框架。在android studio中,編寫在test目錄下的測試類都是基于該框架實現(xiàn),該目錄下的測試代碼運行在本地的JVM上,不需要設(shè)備(真機或模擬器)的支持。
JUnit4中常用的幾個注解:
- @BeforeClass 測試類里所有用例運行之前,運行一次這個方法。方法必須是public static void
- @AfterClass 與BeforeClass對應(yīng)
- @Before 在每個用測試例運行之前都運行一次。
- @After 與Before對應(yīng)
- @Test 指定該方法為測試方法,方法必須是public void
- @RunWith 測試類名之前,用來確定這個類的測試運行器
對于其它的注解,可以通過查看junit4官網(wǎng)來進一步學(xué)習(xí)。
在test下添加測試類,對于需要進行測試的方法添加@Test注解,在該方法中使用assert進行判斷,為了使assert更加直觀,方便,可以使用Hamcrest library,通過使用hamcrest的匹配工具,可以讓你更靈活的進行測試。 以下是一個最簡單的測試類CalculatorTest的實現(xiàn):
public class CalculatorTest {
/** 計算功能類 */
private Calculator mCalculator;
@Before
public void setUp() {
mCalculator = new Calculator();
}
/**
* 測試兩個數(shù)相加
*/
@Test
public void addTwoNumbers() {
double resultAdd = mCalculator.add(1d, 1d);
//使用hamcrest進行assert,直觀,易讀
assertThat(resultAdd, is(equalTo(2d)));
}
……
}


點擊執(zhí)行Run CalculatorTest,整個類的@Test注解的方法都執(zhí)行,或者可以選擇執(zhí)行單個@Test注解方法
當(dāng)需要傳入多個參數(shù)進行條件,即條件覆蓋時,可以使用@Parameters來進行單個方法的多次不同參數(shù)的測試,對應(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;
/** 計算類 */
private Calculator mCalculator;
/**
* 構(gòu)造方法,框架可以自動填充參數(shù)
*/
public CalculatorWithParameterizedTest(double operandOne, double operandTwo,
double expectedResult){
mOperandOne = operandOne;
mOperandTwo = operandTwo;
mExpectedResult = expectedResult;
}
/**
* 需要測試的參數(shù)和對應(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)在目錄下存在如下Test類

如果我們需要同時運行兩個或多個Test類怎么辦?JUnit提供了Suite注解,在對應(yīng)的測試目錄下創(chuàng)建一個空Test類,如Demo里的UnitTestSuite,該類上添加如下注解:
- @RunWith(Suite.class):配置Runner運行環(huán)境。
- @Suite.SuiteClasses({A.class, B.class}):添加需要一起運行的測試類。
/**
* 通過Suite來運行多個Test類
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class, SharedPreferencesHelperWithMockTest.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)用時,你可以通過該方案類完成測試。使用方法是在androidTest目錄下創(chuàng)建測試類,在該類上添加@RunWith(AndroidJUnit4.class)注解。
在Demo中androidTest目錄下的SharedPreferencesHelperTest測試類,該類對SharedPreferencesHelper進行了單元測試,其方法內(nèi)部涉及到了SharedPreferences,該類屬于android系統(tǒng)的api,因此無法直接在test中運行。部分實現(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 Context context;
private SharedPreferences mSharedPreferences;
private SharedPreferenceEntry mSharedPreferenceEntry;
private SharedPreferencesHelper mSharedPreferencesHelper;
@Before
public void setUp() throws Exception{
//獲取application的context
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
//實例化SharedPreferences
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
mSharedPreferenceEntry = new SharedPreferenceEntry(TEST_NAME, TEST_DATE_OF_BIRTH, TEST_EMAIL);
//實例化SharedPreferencesHelper,依賴注入SharePreferences
mSharedPreferencesHelper = new SharedPreferencesHelper(mSharedPreferences);
}
/**
* 測試保存數(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最大的缺點在于無法在本地JVM運行,直接的結(jié)果就是測試速度慢,同時無法執(zhí)行覆蓋測試。因此出現(xiàn)了很多替代方案,比如在設(shè)計合理,依賴注入實現(xiàn)的代碼,可以使用Mockito來進行本地測試,或者使用第三方測試框架Robolectric等。
Mockito
涉及到android依賴的方法的測試,除了在androidTest使用,還可以通過mock來執(zhí)行本地測試。使用Mock的目的主要有以下兩點:
- 驗證這個對象的某些方法的調(diào)用情況,調(diào)用了多少次,參數(shù)是什么等等
- 指定這個對象的某些方法的行為,返回特定的值,或者是執(zhí)行特定的動作
Mockito是優(yōu)秀的mock框架之一,使用該框架可以使mock的操作更加簡單,直觀。
要使用Mockito,需要添加如下依賴:
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation 'junit:junit:4.12'
//如果你要使用Mockito 用于 Android instrumentation tests,那么需要你添加以下三條依賴庫,在版本//mockito-core:2.+以上時dexmaker:1.2和dexmaker-mockito:1.2不好使,需要換成//com.linkedin.dexmaker:dexmaker-mockito:2.25.0
testImplementation 'org.mockito:mockito-core:2.+'
androidTestImplementation 'org.mockito:mockito-core:2.+'
// androidTestImplementation "com.google.dexmaker:dexmaker:1.2"
// androidTestImplementation "com.google.dexmaker:dexmaker-mockito:1.2"
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.25.0'
}
在AndroidJUnitRunner介紹中的對于SharedPreferencesHelper的測試,由于其依賴注入的設(shè)計,我們可以方便的去mock一個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對象的值,同時在代碼中使用到了commit和edit,都需要在方法中進行mock實現(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;
}
……
}
Mocktio的局限
- 不能
mock靜態(tài)方法; - 不能
mock構(gòu)造器; - 不能
mockequals()和hashCode()方法
- 不能
Espresso
在Demo中,除了單元測試的用例,還提供了一個CalculatorInstrumentationTest測試類,該類使用Espresso,一個官方提供了UI測試框架。注意,UI測試不屬于單元測試的范疇。通過Espresso的使用,可以編寫簡潔、運行可靠的自動化UI測試。詳細的使用可以參考測試支持庫中關(guān)于Espresso的使用介紹。
@RunWith(AndroidJUnit4.class)
@LargeTest
public class CalculatorInstrumentationTest {
/**
* 在測試中運行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) {
// 指定輸入框中輸入文本,同時關(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í)行點擊事件
onView(withId(btnOperationResId)).perform(click());
// 獲取文本框中顯示的結(jié)果
onView(withId(R.id.operation_result_text_view)).check(matches(withText(expectedResult)));
}
}
其它
關(guān)于異步操作的單元測試
在實際的android開發(fā)過程中,經(jīng)常涉及到異步操作,比如網(wǎng)絡(luò)請求,Rxjava的線程調(diào)度等。在單元測試中,往往測試方法執(zhí)行往了,異步操作還沒介紹,這就導(dǎo)致了無法順利的執(zhí)行單元測試操作。其解決方法可以提供CountDownLatch類來阻塞測試方法的線程,當(dāng)異步操作完成后(通過回調(diào))來喚醒繼續(xù)執(zhí)行測試,獲取結(jié)果。其實對于網(wǎng)絡(luò)請求這種操作應(yīng)該使用Mock來替代,因為你的單元測試的結(jié)果不應(yīng)受網(wǎng)絡(luò)的影響,不需要關(guān)注網(wǎng)絡(luò)是否正常,服務(wù)器是否崩潰,而應(yīng)該把關(guān)注點放在單元本身的操作。
單元測試,集成測試,UI測試
- UI測試是測試到交互和視覺,以及操作的結(jié)果是否符合預(yù)期??梢酝ㄟ^Espresso,UI Automator等框架,或者人工測試。
- 集成測試是基于單元測試,將多個單元測試組裝起來進行測試,實際測試往往會運行慢,依賴過多導(dǎo)致集成測試非常費時。
- 單元測試僅針對最小單元,在面向?qū)ο笾校瑔卧傅氖欠椒?,包括基類(超類)、抽象類、或者派生類(子類)中的方法?/li>
三者的在實際應(yīng)用中可以通過Test Pyramid(Martin Fowler的總結(jié))來衡量:

所以對于測試,在開發(fā)過程中,我們(開發(fā)者)需要把更多的精力放在單元測試上。
本文轉(zhuǎn)自作者“BooQin”的Android單元測試-常見的方案比較,如有侵權(quán),請聯(lián)系作者效效進行修改。
項目代碼地址:
