沿着金字塔逐級向上,從小型測試到大型測試,各種測試的保真度逐級提升,但維護和調試工做所需的執行時間和工做量也逐級增長。所以,您編寫的單元測試應多於集成測試,集成測試應多於端到端測試。雖然各種測試的比例可能會因應用的用例不一樣而異,但咱們一般建議各種測試所佔比例以下:小型測試佔70%,中型測試佔20%,大型測試佔10%。java
用於驗證應用的行爲,一次驗證一個類。android
原則(F.I.R.S.T
)git
Fast(快),單元測試要運行的足夠快,單個測試方法通常要當即(一秒以內)給出結果。
Idependent(獨立),測試方法之間不要有依賴(先執行某個測試方法,再執行另外一個測試方法才能經過)。
Repeatable(重複),能夠在本地或 CI 不一樣環境(機器上)上反覆執行,不會出現不穩定的狀況。
Self-Validating(自驗證),單元測試必須包含足夠多的斷言進行自我驗證。
Timely(及時),理想狀況下應測試先行,至少保證單元測試應該和實現代碼一塊兒及時完成並提交。github
除此以外,測試代碼應該具有最好的可讀性和最少的維護代價,絕大多數狀況下寫測試應該就像用領域特定語言描述一個事實,甚至不用通過仔細地思考。web
當須要更快地運行測試而不須要與在真實設備上運行測試關聯的保真度和置信度時,可使用本地單元測試來驗證應用的邏輯。編程
若是測試對Android
框架有依賴性(特別是與框架創建複雜交互的測試),則最好使用 Robolectric
添加框架依賴項。json
例:待測試的類同時依賴Context
、Intent
、Bundle
、Application
等Android Framework
中的類時,此時咱們能夠引入Robolectric
框架進行本地單元測試的編寫。
若是測試對Android
框架的依賴性極小,或者若是測試僅取決於咱們本身應用的對象,則可使用諸如Mockito
之類的模擬框架添加模擬依賴項。(BasicUnitAndroidTest)設計模式
例:待測試的類只依賴java api
(最理想的狀況),此時對於待測試類所依賴的其餘類咱們就能夠利用Mockito
框架mock其依賴類,再進行當前類的單元測試編寫。( EmailValidatorTest)例:待測試的類除了依賴
java api
外僅依賴Android Framework
中Context
這個類,此時咱們就能夠利用Mockito
框架mock
Context
類,再進行當前類的單元測試編寫。(SharedPreferencesHelperTest)api
在Android Studio
項目中,本地單元測試的源文件存儲在module-name/src/test/java/
中。架構
在模塊的頂級build.gradle
文件中,將如下庫指定爲依賴項:
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
選項。而後,單元測試能夠訪問編譯版本的資源,從而使測試更快速且更準確地運行。
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() }
若是單元測試包含異步操做時,可使用awaitility庫進行測試;當使用RxJava響應式編程庫時,能夠自定義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)) ) } }
插樁單元測試是在物理設備和模擬器上運行的測試,此類測試能夠利用Android
框架API
。插樁測試提供的保真度比本地單元測試要高,但運行速度要慢得多。所以,咱們建議只有在必須針對真實設備的行爲進行測試時才使用插樁單元測試。
在Android Studio
項目中,插樁測試的源文件存儲在module-name/src/androidTest/java/
。
在模塊的頂級build.gradle
文件中,將如下庫指定爲依賴項:
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() }
總結:
MVP
、MVVM
架構設計模式,MVP
中Model
層和Presenter
層儘可能不依賴Android Framework
,MVVM
中Model
層和ViewModel
層儘可能不依賴Android Framework
。Android Framework API
很是少時,能夠採用Mock Android api
的方式。Android Framework API
時,引入Robolectric
庫模擬Android
環境或者放入AndroidTest目錄做爲插樁單元測試在物理設備上跑。Robolectric
庫寫本地單元測試時,依賴的某些類的方法調用出問題致使測試failed
時,可使用shadow
類提供默認實現。Given
、When
、Then
的方式進行區分.@Test public void should_do_something_if_some_condition_fulfills() { // Given 設置前置條件 // When 執行被測方法 // Then 驗證方法結果 }
用於驗證模塊內堆棧級別之間的交互或相關模塊之間的交互
Service
或ContentProvider
),應驗證這些組件在應用中的行爲是否正確。參考插樁單元測試環境設置
ServiceTestRule
,可在單元測試方法運行以前啓動服務,並在測試完成後關閉服務。ServiceTestRule
類不支持測試IntentService
對象。若是須要測試IntentService
對象,能夠應將邏輯封裝在一個單獨的類中,並建立相應的單元測試。@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))); } }
使用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
提供單獨的測試用例類。要驗證 BroadcastReceiver
是否正確響應,能夠測試向其發送Intent
對象的組件。或者,能夠經過調用ApplicationProvider.getApplicationContext()
來建立BroadcastReceiver
的實例,而後調用要測試的BroadcastReceiver
方法(一般,這是onReceive()
方法)用於驗證跨越了應用的多個模塊的用戶操做流程
界面測試的一種方法是直接讓測試人員對目標應用執行一系列用戶操做,並驗證其行爲是否正常。不過,這種人工方法會很是耗時、繁瑣且容易出錯。一種更高效的方法是編寫界面測試,以便以自動化方式執行用戶操做。自動化方法能夠以可重複的方式快速可靠地運行測試。
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" }
Activity
中輸入特定內容時的行爲是否符合預期。它可以讓您檢查目標應用是否返回正確的界面輸出來響應應用 Activity
中的用戶交互。諸如 Espresso
之類的界面測試框架可以讓您以編程方式模擬用戶操做,並測試複雜的應用內用戶交互。(espresso測試單個應用的界面例子)Android
相冊應用正確分享圖片。支持跨應用交互的界面測試框架(如 UI Automator
)可以讓您針對此類場景建立測試。(uiautomator測試多個應用的界面)