測試金字塔

沿著金字塔逐級向上,從小型測試到大型測試,各類測試的保真度逐級提高,但維護(hù)和調(diào)試工作所需的執(zhí)行時(shí)間和工作量也逐級增加。因此,您編寫的單元測試應(yīng)多于集成測試,集成測試應(yīng)多于端到端測試。雖然各類測試的比例可能會(huì)因應(yīng)用的用例不同而異,但我們通常建議各類測試所占比例如下:小型測試占70%,中型測試占20%,大型測試占10%。
單元測試(小型測試)
用于驗(yàn)證應(yīng)用的行為,一次驗(yàn)證一個(gè)類。
原則(F.I.R.S.T)
Fast(快),單元測試要運(yùn)行的足夠快,單個(gè)測試方法一般要立即(一秒之內(nèi))給出結(jié)果。
Idependent(獨(dú)立),測試方法之間不要有依賴(先執(zhí)行某個(gè)測試方法,再執(zhí)行另一個(gè)測試方法才能通過)。
Repeatable(重復(fù)),可以在本地或 CI 不同環(huán)境(機(jī)器上)上反復(fù)執(zhí)行,不會(huì)出現(xiàn)不穩(wěn)定的情況。
Self-Validating(自驗(yàn)證),單元測試必須包含足夠多的斷言進(jìn)行自我驗(yàn)證。
Timely(及時(shí)),理想情況下應(yīng)測試先行,至少保證單元測試應(yīng)該和實(shí)現(xiàn)代碼一起及時(shí)完成并提交。
除此之外,測試代碼應(yīng)該具備最好的可讀性和最少的維護(hù)代價(jià),絕大多數(shù)情況下寫測試應(yīng)該就像用領(lǐng)域特定語言描述一個(gè)事實(shí),甚至不用經(jīng)過仔細(xì)地思考。
構(gòu)建本地單元測試
當(dāng)需要更快地運(yùn)行測試而不需要與在真實(shí)設(shè)備上運(yùn)行測試關(guān)聯(lián)的保真度和置信度時(shí),可以使用本地單元測試來驗(yàn)證應(yīng)用的邏輯。
-
如果測試對
Android框架有依賴性(特別是與框架建立復(fù)雜交互的測試),則最好使用Robolectric添加框架依賴項(xiàng)。例:待測試的類同時(shí)依賴
Context、Intent、Bundle、Application等Android Framework中的類時(shí),此時(shí)我們可以引入Robolectric框架進(jìn)行本地單元測試的編寫。 -
如果測試對
Android框架的依賴性極小,或者如果測試僅取決于我們自己應(yīng)用的對象,則可以使用諸如Mockito之類的模擬框架添加模擬依賴項(xiàng)。(BasicUnitAndroidTest)例:待測試的類只依賴
java api(最理想的情況),此時(shí)對于待測試類所依賴的其他類我們就可以利用Mockito框架mock其依賴類,再進(jìn)行當(dāng)前類的單元測試編寫。(EmailValidatorTest)例:待測試的類除了依賴
java api外僅依賴Android Framework中Context這個(gè)類,此時(shí)我們就可以利用Mockito框架mockContext類,再進(jìn)行當(dāng)前類的單元測試編寫。(SharedPreferencesHelperTest)
設(shè)置測試環(huán)境
在Android Studio項(xiàng)目中,本地單元測試的源文件存儲(chǔ)在module-name/src/test/java/中。
在模塊的頂級build.gradle文件中,將以下庫指定為依賴項(xiàng):
dependencies {
// Required -- JUnit 4 framework
testImplementation "junit:junit:$junitVersion"
// Optional -- Mockito framework
testImplementation "org.mockito:mockito-core:$mockitoCoreVersion"
// Optional -- Robolectric environment
testImplementation "androidx.test:core:$xcoreVersion"
testImplementation "androidx.test.ext:junit:$extJunitVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
}
如果單元測試依賴于資源,需要在module的build.gradle文件中啟用includeAndroidResources選項(xiàng)。然后,單元測試可以訪問編譯版本的資源,從而使測試更快速且更準(zhǔn)確地運(yùn)行。
android {
// ...
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
@RunWith(AndroidJUnit4::class)
@Config(manifest = Config.NONE)
class PeopleDaoTest {
private lateinit var database: PeopleDatabase
private lateinit var peopleDao: PeopleDao
@Before
fun `create db`() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
PeopleDatabase::class.java
).allowMainThreadQueries().build()
peopleDao = database.peopleDao()
}
@Test
fun `should return empty list when getPeople without inserted data`() {
val result = peopleDao.getPeople(pageId = 1)
assertThat(result).isNotNull()
assertThat(result).isEmpty()
}
如果單元測試包含異步操作時(shí),可以使用awaitility庫進(jìn)行測試;當(dāng)使用RxJava響應(yīng)式編程庫時(shí),可以自定義rule:
class RxJavaRule : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
RxJavaPlugins.setIoSchedulerHandler {
Schedulers.trampoline()
}
RxJavaPlugins.setNewThreadSchedulerHandler {
Schedulers.trampoline()
}
RxJavaPlugins.setComputationSchedulerHandler {
Schedulers.trampoline()
}
RxAndroidPlugins.setMainThreadSchedulerHandler {
Schedulers.trampoline()
}
RxAndroidPlugins.setInitMainThreadSchedulerHandler {
Schedulers.trampoline()
}
}
override fun finished(description: Description?) {
super.finished(description)
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
TestScheduler中triggerActions的使用。
@RunWith(JUnit4::class)
class FilmViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val rxJavaRule = RxJavaRule()
private val repository = mock(Repository::class.java)
private val testScheduler = TestScheduler()
private lateinit var viewModel: FilmViewModel
@Before
fun init() {
viewModel = FilmViewModel(repository)
}
@Test
fun `should return true when loadFilms is loading`() {
`when`(repository.getPopularFilms(1)).thenReturn(
Single.just(emptyList<Film>())
.subscribeOn(testScheduler)
)
viewModel.loadFilms(0)
assertThat(getValue(viewModel.isLoading)).isTrue()
testScheduler.triggerActions()
assertThat(getValue(viewModel.isLoading)).isFalse()
}
@Test
fun `should return films list when loadFilms successful`() {
`when`(repository.getPopularFilms(1)).thenReturn(
Single.just(
listOf(
Film(123, "", "", "", "", "", "", 1)
)
).subscribeOn(testScheduler)
)
viewModel.loadFilms(0)
assertThat(getValue(viewModel.films)).isNull()
testScheduler.triggerActions()
assertThat(getValue(viewModel.films)).isNotNull()
assertThat(getValue(viewModel.films).size).isEqualTo(1)
}
}
TestSubscriber的使用。
@RunWith(JUnit4::class)
class WebServiceTest {
private lateinit var webService: WebService
private lateinit var mockWebServer: MockWebServer
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@Before
fun `start service`() {
mockWebServer = MockWebServer()
webService = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
.create(WebService::class.java)
}
@Test
fun `should return fim list when getFilms successful`() {
assertThat(webService).isNotNull()
enqueueResponse("popular_films.json")
val testObserver = webService.getPopularFilms(page = 1)
.map {
it.data
}.test()
testObserver.assertNoErrors()
testObserver.assertValueCount(1)
testObserver.assertValue {
assertThat(it).isNotEmpty()
assertThat(it[0].id).isEqualTo(297761)
assertThat(it[1].id).isEqualTo(324668)
it.size == 2
}
testObserver.assertComplete()
testObserver.dispose()
}
@After
fun `stop service`() {
mockWebServer.shutdown()
}
private fun enqueueResponse(fileName: String) {
val inputStream = javaClass.classLoader?.getResourceAsStream("api-response/$fileName")
?: return
val source = inputStream.source().buffer()
val mockResponse = MockResponse()
mockWebServer.enqueue(
mockResponse
.setBody(source.readString(Charsets.UTF_8))
)
}
}
構(gòu)建插樁單元測試
插樁單元測試是在物理設(shè)備和模擬器上運(yùn)行的測試,此類測試可以利用Android框架API。插樁測試提供的保真度比本地單元測試要高,但運(yùn)行速度要慢得多。因此,我們建議只有在必須針對真實(shí)設(shè)備的行為進(jìn)行測試時(shí)才使用插樁單元測試。
設(shè)置測試環(huán)境
在Android Studio項(xiàng)目中,插樁測試的源文件存儲(chǔ)在module-name/src/androidTest/java/。
在模塊的頂級build.gradle文件中,將以下庫指定為依賴項(xiàng):
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test:core:$xcoreVersion"
androidTestImplementation "androidx.test:rules:$rulesVersion"
// Optional -- Truth library
androidTestImplementation "androidx.test.ext:truth:$androidxtruthVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoCoreVersion"
androidTestImplementation "org.mockito:mockito-android:$mockitoAndroidVersion"
}
@RunWith(AndroidJUnit4::class)
@SmallTest
class FilmDaoTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var database: FilmDatabase
private lateinit var filmDao: FilmDao
@Before
fun initDb() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
FilmDatabase::class.java
).build()
filmDao = database.filmData()
}
@Test
fun should_return_film_list_when_getFilms_with_inserted_film_list() {
filmDao.insert(
Film(100, "", "", "", "", "", "", 1)
)
filmDao.insert(
Film(101, "", "", "", "", "", "", 1)
)
val result = filmDao.getFilms(1)
assertThat(result).isNotNull()
assertThat(result).isNotEmpty()
assertThat(result.size).isEqualTo(2)
assertThat(result[0].id).isEqualTo(100)
assertThat(result[0].page).isEqualTo(1)
assertThat(result[1].id).isEqualTo(101)
}
@Test
fun should_return_film_list_with_size_1_when_getFilms_with_inserted_2_same_film() {
filmDao.insert(
Film(100, "", "", "", "", "", "", 1)
)
filmDao.insert(
Film(100, "1223", "111", "", "", "", "", 1)
)
val result = filmDao.getFilms(1)
assertThat(result).isNotNull()
assertThat(result).isNotEmpty()
assertThat(result.size).isEqualTo(1)
assertThat(result[0].id).isEqualTo(100)
assertThat(result[0].page).isEqualTo(1)
}
@Test
fun should_return_empty_list_when_getFilms_with_deleteAll_called() {
filmDao.insert(
Film(100, "", "", "", "", "", "", 1)
)
filmDao.deleteAll()
val newResult = filmDao.getFilms(1)
assertThat(newResult).isNotNull()
assertThat(newResult).isEmpty()
}
@After
fun closeDb() = database.close()
}
總結(jié):
- 基于目前流行的
MVP、MVVM架構(gòu)設(shè)計(jì)模式,MVP中Model層和Presenter層盡量不依賴Android Framework,MVVM中Model層和ViewModel層盡量不依賴Android Framework。 - 類的設(shè)計(jì)做到單一職責(zé)原則,依賴其他類時(shí)提供方便mock的方式(例如作為構(gòu)造方法參數(shù)傳遞),某一個(gè)方法依賴其他對象時(shí),小重構(gòu)該對象作為方法參數(shù)傳入。
- 方法盡量短小(方法太長時(shí)可以利用重構(gòu)手法在方法中再提取方法)。
- 只覆蓋public方法單元測試,privite方法可以間接測試。
- 當(dāng)依賴
Android Framework API非常少時(shí),可以采用Mock Android api的方式。 - 當(dāng)嚴(yán)重依賴
Android Framework API時(shí),引入Robolectric庫模擬Android環(huán)境或者放入AndroidTest目錄作為插樁單元測試在物理設(shè)備上跑。 - 使用
Robolectric庫寫本地單元測試時(shí),依賴的某些類的方法調(diào)用出問題導(dǎo)致測試failed時(shí),可以使用shadow類提供默認(rèn)實(shí)現(xiàn)。 - 每條測試采用
Given、When、Then的方式進(jìn)行區(qū)分.
@Test
public void should_do_something_if_some_condition_fulfills() {
// Given 設(shè)置前置條件
// When 執(zhí)行被測方法
// Then 驗(yàn)證方法結(jié)果
}
集成測試(中型測試)
用于驗(yàn)證模塊內(nèi)堆棧級別之間的交互或相關(guān)模塊之間的交互
- 如果應(yīng)用使用了用戶不直接與之交互的組件(如
Service或ContentProvider),應(yīng)驗(yàn)證這些組件在應(yīng)用中的行為是否正確。
設(shè)置測試環(huán)境
參考插樁單元測試環(huán)境設(shè)置
Service測試
- 利用
ServiceTestRule,可在單元測試方法運(yùn)行之前啟動(dòng)服務(wù),并在測試完成后關(guān)閉服務(wù)。 ServiceTestRule類不支持測試IntentService對象。如果需要測試IntentService對象,可以應(yīng)將邏輯封裝在一個(gè)單獨(dú)的類中,并創(chuàng)建相應(yīng)的單元測試。
@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(getApplicationContext(), 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)));
}
}
ContentProvider的測試
使用ProviderTestRule
@Rule
public ProviderTestRule mProviderRule =
new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY).build();
@Test
public void verifyContentProviderContractWorks() {
ContentResolver resolver = mProviderRule.getResolver();
// perform some database (or other) operations
Uri uri = resolver.insert(testUrl, testContentValues);
// perform some assertions on the resulting URI
assertNotNull(uri);
}
@Rule
public ProviderTestRule mProviderRule =
new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY)
.setDatabaseCommands(DATABASE_NAME, INSERT_ONE_ENTRY_CMD, INSERT_ANOTHER_ENTRY_CMD)
.build();
@Test
public void verifyTwoEntriesInserted() {
ContentResolver mResolver = mProviderRule.getResolver();
// two entries are already inserted by rule, we can directly perform assertions to verify
Cursor c = null;
try {
c = mResolver.query(URI_TO_QUERY_ALL, null, null, null, null);
assertNotNull(c);
assertEquals(2, c.getCount());
} finally {
if (c != null && !c.isClosed()) {
c.close();
}
}
}
-
Android沒有為BroadcastReceiver提供單獨(dú)的測試用例類。要驗(yàn)證BroadcastReceiver是否正確響應(yīng),可以測試向其發(fā)送Intent對象的組件?;蛘?,可以通過調(diào)用ApplicationProvider.getApplicationContext()來創(chuàng)建BroadcastReceiver的實(shí)例,然后調(diào)用要測試的BroadcastReceiver方法(通常,這是onReceive()方法)
端到端測試(大型測試)
用于驗(yàn)證跨越了應(yīng)用的多個(gè)模塊的用戶操作流程
界面測試的一種方法是直接讓測試人員對目標(biāo)應(yīng)用執(zhí)行一系列用戶操作,并驗(yàn)證其行為是否正常。不過,這種人工方法會(huì)非常耗時(shí)、繁瑣且容易出錯(cuò)。一種更高效的方法是編寫界面測試,以便以自動(dòng)化方式執(zhí)行用戶操作。自動(dòng)化方法可以以可重復(fù)的方式快速可靠地運(yùn)行測試。
設(shè)置測試環(huán)境
dependencies {
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test:core:$xcoreVersion"
androidTestImplementation "androidx.test:rules:$rulesVersion"
// Optional -- Truth library
androidTestImplementation "androidx.test.ext:truth:$androidxtruthVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoCoreVersion"
androidTestImplementation "org.mockito:mockito-android:$mockitoAndroidVersion"
// Optional -- UI testing with Espresso
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
// Optional -- UI testing with UI Automator
androidTestImplementation "androidx.test.uiautomator:uiautomator:$uiautomatorVersion"
}
涵蓋單個(gè)應(yīng)用的界面測試:這種類型的測試可驗(yàn)證目標(biāo)應(yīng)用在用戶執(zhí)行特定操作或在其
Activity中輸入特定內(nèi)容時(shí)的行為是否符合預(yù)期。它可讓您檢查目標(biāo)應(yīng)用是否返回正確的界面輸出來響應(yīng)應(yīng)用Activity中的用戶交互。諸如Espresso之類的界面測試框架可讓您以編程方式模擬用戶操作,并測試復(fù)雜的應(yīng)用內(nèi)用戶交互。(espresso測試單個(gè)應(yīng)用的界面例子)涵蓋多個(gè)應(yīng)用的界面測試:這種類型的測試可驗(yàn)證不同用戶應(yīng)用之間交互或用戶應(yīng)用與系統(tǒng)應(yīng)用之間交互的正確行為。例如,您可能想要測試相機(jī)應(yīng)用是否能夠與第三方社交媒體應(yīng)用或默認(rèn)的
Android相冊應(yīng)用正確分享圖片。支持跨應(yīng)用交互的界面測試框架(如UI Automator)可讓您針對此類場景創(chuàng)建測試。(uiautomator測試多個(gè)應(yīng)用的界面)