Setups
Instrumented Test是基于JUnit的,使用JUnit4的Test Class風(fēng)格我們可以可以很快的寫(xiě)出Instrumented Test Class。當(dāng)然在此之前结序,我們還需要做一些環(huán)境的配置工作。包括:
- 配置 instrumentation runner刚盈。這個(gè)runner負(fù)責(zé)執(zhí)行Instrumented Test Class仪糖!
android { defaultConfig { ... testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } }
- 配置需要的dependencies,這里將需要的依賴都列出來(lái)了忙上,包括基本依賴拷呆、UI測(cè)試的依賴和其他庫(kù)。
dependencies { //這個(gè)庫(kù)是為了避免因?yàn)橹鞒绦蛞蕾嚵讼嗤瑤?kù)的不同版本導(dǎo)致沖突加入的 androidTestCompile 'com.android.support:support-annotations:24.0.0' //必需的依賴疫粥; androidTestCompile 'com.android.support.test:runner:0.5' //可選依賴茬斧,包含Android提供的幾個(gè)Rule實(shí)現(xiàn),如ActivityTestRule梗逮,ServiceTestRule androidTestCompile 'com.android.support.test:rules:0.5' // 可選-- Hamcrest library项秉,matcher庫(kù),可以很方便的構(gòu)建matcher androidTestCompile 'org.hamcrest:hamcrest-library:1.3' // 可選-- UI testing with Espresso UI測(cè)試時(shí)使用 androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' // 可選 -- UI testing with UI Automator 跨App UI測(cè)試時(shí)使用 androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}
##Test UI
主要是使用Espresso庫(kù)來(lái)進(jìn)行UI測(cè)試慷彤。Espresso能夠自動(dòng)test action和target app的UI狀態(tài)娄蔼,它會(huì)在UI線程Idle的時(shí)候去執(zhí)行test code,避免開(kāi)發(fā)者自己去做同步的工作瞬欧,提高了測(cè)試的可靠性贷屎。在進(jìn)行UI測(cè)試時(shí),可能需要在開(kāi)發(fā)者選項(xiàng)中將窗口動(dòng)畫(huà)縮放艘虎、過(guò)渡動(dòng)畫(huà)縮放和動(dòng)畫(huà)時(shí)長(zhǎng)調(diào)整選項(xiàng)關(guān)閉唉侄,否則可能會(huì)導(dǎo)致不可預(yù)料的結(jié)果或者直接導(dǎo)致測(cè)試失敗。Espresso的使用主要分為幾個(gè)步驟:
1. 使用`onView()`或者`onData()`方法(針對(duì)AdapterView)找到對(duì)應(yīng)的view野建。`onView()`方法接收一個(gè)`Matcher<View>`對(duì)象作為參數(shù)用來(lái)在當(dāng)前的視圖層次結(jié)構(gòu)中找到對(duì)應(yīng)的view属划,找不到或者有多個(gè)View滿足條件都會(huì)拋出異常『蛏可以查看 [ViewMatchers](https://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers.html) 看看Espresso提供的matchers同眯。`onData()`主要適用于AdapterView,比如ListView唯鸭、GridView须蜗、Spinner。這些控件可能包含很多的item view目溉,使用`onView()`方法去找明肮,不一定能找到,因?yàn)閕tem view可能不在當(dāng)前視圖層次結(jié)構(gòu)中顯示缭付。所以espresso提供`onData()`方法使用柿估。該方法接收一個(gè)`Matcher<Object>`類(lèi)型的參數(shù)用來(lái)匹配對(duì)應(yīng)的Item。其實(shí)就是在對(duì)應(yīng)的Adapter上調(diào)用`getItem()`方法陷猫,用返回的Object對(duì)象去匹配那個(gè)Matcher對(duì)象秫舌。找到這個(gè)Item的妖,espresso會(huì)自動(dòng)滾動(dòng)到該item的位置。
```java
onView(withText("Sign-in"));
onView(withId(R.id.button_signin));
onView(allOf(withId(R.id.button_signin), withText("Sign-in")));
onData(allOf(is(instanceOf(Map.class)),
hasEntry(equalTo(LongListActivity.ROW_TEXT), is(str))));
- 通過(guò)上面兩個(gè)方法的返回對(duì)View進(jìn)行操作足陨。
onView()
方法會(huì)返回一個(gè)ViewInteraction
對(duì)象嫂粟,而onData()
方法返回一個(gè)DataInteraction
對(duì)象。兩個(gè)對(duì)象都有一個(gè)perform()
方法钠右,該方法接收變長(zhǎng)的ViewAction
對(duì)象作為參數(shù)赋元,可以連續(xù)執(zhí)行一些的操作,諸如點(diǎn)擊飒房、滑動(dòng)搁凸、輸入等操作。具體可執(zhí)行的操作由具體的view決定狠毯,查看 ViewActions看看可使用什么操作护糖。 - 在
ViewInteraction
或者DataInteraction
上調(diào)用check()
方法驗(yàn)證View的狀態(tài);該方法接收一個(gè)ViewAssertion
作為參數(shù)嚼松,查看 ViewAssertions提供了哪些方法幫助構(gòu)建ViewAssertion
嫡良。
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher
.perform(click()) // click() is a ViewAction
.check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
Intents Stub
需要添加espresso-intents依賴庫(kù)。
@Rule //需要聲明使用這個(gè)Rule献酗,繼承于ActivityTestRule寝受,但會(huì)在前后做一些Intents Stub的準(zhǔn)備和清理工作
public IntentsTestRule<DialerActivity> mActivityRule = new IntentsTestRule<>(DialerActivity.class);
//如果有匹配matcher的Intent請(qǐng)求,則用result響應(yīng)
//在啟動(dòng)外部某個(gè)app罕偎,希望得到響應(yīng)時(shí)很有用
intending(Matcher<Intent> matcher).respondWith(ActivityResult result);
//驗(yàn)證當(dāng)前的正在測(cè)試的application發(fā)送了一個(gè)matcher中指定的Intent
//重載的方法允許驗(yàn)證匹配的Intent發(fā)送了多次
intended(Matcher<Intent> matcher)
intended(Matcher<Intent> matcher, times(2));
Test Service
Service的測(cè)試需要用到ServiceTestRule
很澄,這個(gè)rule幫助我們start或者成功bind到service,在結(jié)束測(cè)試后還能幫助我們stop或者unbind颜及。這里只給出Test Class的代碼甩苛,完整的代碼在這里。
@MediumTest
@RunWith(AndroidJUnit4.class)
public class LocalServiceTest {
@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule();
@Test
public void testWithBoundService() throws TimeoutException {
// Create the service Intent.
Intent serviceIntent =
new Intent(InstrumentationRegistry.getTargetContext(), LocalService.class);
// Data can be passed to the service via the Intent.
serviceIntent.putExtra(LocalService.SEED_KEY, 42L);
// Bind the service and grab a reference to the binder.
IBinder binder = mServiceRule.bindService(serviceIntent);
// Get the reference to the service, or you can call public methods on the binder directly.
LocalService service = ((LocalService.LocalBinder) binder).getService();
// Verify that the service is working correctly.
assertThat(service.getRandomInt(), is(any(Integer.class)));
}
}
Test Broadcast
Android沒(méi)有為Broadcast提供類(lèi)似ServiceTestRule這樣的Rule俏站,因?yàn)檫@根本不需要讯蒲。一般如果需要測(cè)試一個(gè)Broadcast,則直接創(chuàng)建這個(gè)這個(gè)receiver的實(shí)例肄扎,然后調(diào)用onReceive()
方法墨林,最后驗(yàn)證一些信息。
public class LocalReceiverTest {
@Test
public void testOnReceive() throws Exception {
LocalReceiver localReceiver = new LocalReceiver();
Intent intent = new Intent();
intent.putExtra("key", "I Love You!");//onReceive() just saved the key to somewhere
localReceiver.onReceive(InstrumentationRegistry.getTargetContext(), intent);
//do some asserts
assertEquals(getKeyFromSomeWhere(), "I Love You!");
}
}
Test ContentProvider
ContentProvider的測(cè)試比較特殊犯祠,我們需要在獨(dú)立的測(cè)試環(huán)境中測(cè)試從而不影響真實(shí)的用戶數(shù)據(jù)萌丈。具體如何測(cè)試,Android官方給了詳細(xì)說(shuō)明雷则。想要查看例子可以看這里。
Test File or DataBase
文件或者數(shù)據(jù)庫(kù)的測(cè)試需要注意兩個(gè)測(cè)試之間不能相互影響肪笋,且不能影響到正常的數(shù)據(jù)庫(kù)月劈。這就要求我們自己建立一個(gè)測(cè)試環(huán)境度迂,好在使用RenamingDelegatingContext
能夠滿足上面的要求。RenamingDelegatingContext
會(huì)將其他操作委托給一個(gè)給定的context
對(duì)象猜揪,但在執(zhí)行數(shù)據(jù)庫(kù)/文件相關(guān)的操作時(shí)惭墓,會(huì)用一個(gè)prefix重命名給定的數(shù)據(jù)庫(kù)名或者文件名。所以使用這個(gè)context而姐,我們操作的只是一個(gè)測(cè)試數(shù)據(jù)庫(kù)或者文件腊凶。
public class MyDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "database.db";
private static final int DATABASE_VERSION = 1;
public MyDatabase(Context context){
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db){
// some code
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// some code
}
}
public class MyDatabaseTest {
private MyDatabase db;
@Before
public void setUp() throws Exception {
RenamingDelegatingContext context = new RenamingDelegatingContext(InstrumentationRegistry.getTargetContext(), "test_");
//每次測(cè)試時(shí),刪除舊的database文件
context.deleteDatabase("database.db");
//使用這個(gè)context創(chuàng)建database拴念,文件名對(duì)應(yīng)為'test_database.db'
db = new MyDatabase(context);
}
@After
public void tearDown() throws Exception {
db.close();
}
//@Test
public void testAddEntry(){
// Here i have my new database wich is not connected to the standard database of the App
...
}
}