Android自動化測試

Android自動化測試

測試金字塔

測試金字塔,顯示了應用的測試套件應包含的三類測試
沿着金字塔逐級向上,從小型測試到大型測試,各種測試的保真度逐級提升,但維護和調試工做所需的執行時間和工做量也逐級增長。所以,您編寫的單元測試應多於集成測試,集成測試應多於端到端測試。雖然各種測試的比例可能會因應用的用例不一樣而異,但咱們一般建議各種測試所佔比例以下:小型測試佔70%,中型測試佔20%,大型測試佔10%java

單元測試(小型測試)

用於驗證應用的行爲,一次驗證一個類。android

原則(F.I.R.S.Tgit

Fast(快),單元測試要運行的足夠快,單個測試方法通常要當即(一秒以內)給出結果。
Idependent(獨立),測試方法之間不要有依賴(先執行某個測試方法,再執行另外一個測試方法才能經過)。
Repeatable(重複),能夠在本地或 CI 不一樣環境(機器上)上反覆執行,不會出現不穩定的狀況。
Self-Validating(自驗證),單元測試必須包含足夠多的斷言進行自我驗證。
Timely(及時),理想狀況下應測試先行,至少保證單元測試應該和實現代碼一塊兒及時完成並提交。github

除此以外,測試代碼應該具有最好的可讀性和最少的維護代價,絕大多數狀況下寫測試應該就像用領域特定語言描述一個事實,甚至不用通過仔細地思考web

構建本地單元測試

當須要更快地運行測試而不須要與在真實設備上運行測試關聯的保真度和置信度時,可使用本地單元測試來驗證應用的邏輯。編程

  • 若是測試對Android框架有依賴性(特別是與框架創建複雜交互的測試),則最好使用 Robolectric添加框架依賴項。json

    例:待測試的類同時依賴 ContextIntentBundleApplicationAndroid Framework中的類時,此時咱們能夠引入 Robolectric框架進行本地單元測試的編寫。
  • 若是測試對Android框架的依賴性極小,或者若是測試僅取決於咱們本身應用的對象,則可使用諸如Mockito之類的模擬框架添加模擬依賴項。(BasicUnitAndroidTest)設計模式

    例:待測試的類只依賴 java api(最理想的狀況),此時對於待測試類所依賴的其餘類咱們就能夠利用 Mockito框架mock其依賴類,再進行當前類的單元測試編寫。( EmailValidatorTest)

    例:待測試的類除了依賴java api外僅依賴Android FrameworkContext這個類,此時咱們就能夠利用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()
    }
}

TestSchedulertriggerActions的使用。

@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()
}

總結

  • 基於目前流行的MVPMVVM架構設計模式,MVPModel層和Presenter層儘可能不依賴Android FrameworkMVVMModel層和ViewModel層儘可能不依賴Android Framework
  • 類的設計作到單一職責原則,依賴其餘類時提供方便mock的方式(例如做爲構造方法參數傳遞),某一個方法依賴其餘對象時,小重構該對象做爲方法參數傳入。
  • 方法儘可能短小(方法太長時能夠利用重構手法在方法中再提取方法)。
  • 只覆蓋public方法單元測試,privite方法能夠間接測試。
  • 當依賴Android Framework API很是少時,能夠採用Mock Android api的方式。
  • 當嚴重依賴Android Framework API時,引入Robolectric庫模擬Android環境或者放入AndroidTest目錄做爲插樁單元測試在物理設備上跑。
  • 使用Robolectric庫寫本地單元測試時,依賴的某些類的方法調用出問題致使測試failed時,可使用shadow類提供默認實現。
  • 每條測試採用GivenWhenThen的方式進行區分.
@Test
public void should_do_something_if_some_condition_fulfills() {
    // Given 設置前置條件

    // When 執行被測方法

    // Then 驗證方法結果
}

集成測試(中型測試)

用於驗證模塊內堆棧級別之間的交互或相關模塊之間的交互

  • 若是應用使用了用戶不直接與之交互的組件(如ServiceContentProvider),應驗證這些組件在應用中的行爲是否正確。

設置測試環境

參考插樁單元測試環境設置

Service測試

  • 利用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)));
    }
}

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提供單獨的測試用例類。要驗證 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測試多個應用的界面

參考例子testing-samples

相關文章
相關標籤/搜索