Android自動(dòng)化測試

測試金字塔

測試金字塔,顯示了應(yīng)用的測試套件應(yīng)包含的三類測試

沿著金字塔逐級向上,從小型測試到大型測試,各類測試的保真度逐級提高,但維護(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、BundleApplicationAndroid 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 FrameworkContext這個(gè)類,此時(shí)我們就可以利用Mockito框架mock Context類,再進(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()
    }
}

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

構(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é)

  • 基于目前流行的MVPMVVM架構(gòu)設(shè)計(jì)模式,MVPModel層和Presenter層盡量不依賴Android FrameworkMVVMModel層和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)用使用了用戶不直接與之交互的組件(如ServiceContentProvider),應(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)用的界面

參考例子testing-samples

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容