對於大多數 Android 商業項目,基本都是處於高速迭代的開發階段,這個階段不只僅是對項目的開發效率,也對項目的產品質量提出了更高的要求。java
一般大型項目都是經過黑盒測試等方式來提供質量相關的保障,但同時筆者認爲也須要 Android 端的單元測試以及能自動在 Android 平臺上運行的 UI 測試,這幾種測試有如下幾個優點:android
在 Android Studio 中新建新的項目時,它已自動爲兩種測試類型建立了對應的代碼目錄:數據庫
接下來,筆者將嘗試爲本身的項目(基於 MVP 架構開發)補充相應的單元測試用例和 UI 測試用例,來初步實踐下如何在 Android 平臺編寫和運行相關的測試用例。網絡
若是須要編寫一個新的本地單元測試用例,只需打開你想測試的 java 代碼文件,而後點擊類名 – ⇧⌘T(Windows:Ctrl+Shift+T)– 選擇要生成的方法 – 選擇 test 文件夾,對應於本地單元測試 – 完成。架構
須要 JUnit 和 Mockito 框架支持,因此在 build.gradle 中增長:app
testImplementation "junit:junit:4.12" testImplementation "org.mockito:mockito-core:2.7.1"
通常來講,編寫一段測試代碼須要三個步驟:框架
筆者主要測試的是 MVP 架構中 P 層的代碼。在筆者的項目中,P 層是經過 Dagger2 機制,注入一個 DataManager,也就是數據獲取源。同時也須要一個 V 層的代理,這樣在 P 層經過數據源獲取數據以後,就能將數據交給 V 層,由 V 層去展現。異步
代碼調用大體邏輯以下:ide
mPresenter = new NewsPresenter(mDataManager); mPresenter.getNews(); mPresenter.attach(mView); --> mView.showProgress(); // 在數據未加載完前加載進度條 --> mView.showNews(news); --> mView.hideProgress(); // 在數據加載完後隱藏進度條
對應着,實際編寫 P 層的單元測試用例的時候,並不須要一個真實的數據源,只須要經過 Mockito 框架,mock 出一個測試用的 DataManager 和 V 層代理。單元測試
對應着 Presenter 類,新建立的測試代碼以下:
/** * Created by Xu on 2019/04/05. * * @author Xu */ public class NewsPresenterTest { @ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule(); @Mock private NewsContract.View view; @Mock protected DataManager mMockDataManager; private NewsPresenter newsPresenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); newsPresenter = new NewsPresenter(mMockDataManager); newsPresenter.attach(view); } @Test public void getNewsAndLoadIntoView() { TencentNewsResultBean resultBean = new TencentNewsResultBean(); resultBean.setData(new ArrayList<>()); when(mMockDataManager.getNews()).thenReturn(Flowable.just(resultBean)); newsPresenter.getNews(); // 測試model是否有獲取數據 verify(mMockDataManager).getNews(); // 測試view是否調用相應接口 verify(view).showProgress(); verify(view).showNews(anyList()); verify(view).hideProgress(); } @After public void tearDown() { newsPresenter.detach(); } }
在其中:
什麼是 @ClassRule 呢?它跟 @Rule 註解幾乎相同,能夠在全部類方法開始前進行一些相關的初始化調用操做。使用這個註解,能夠在執行測試用例的時候加入特有的操做,而不影響原有用例代碼,有效減小耦合程度。
這裏主要是由於項目中使用了 RxJava2,而 RxJava 是須要 Android 環境支持的,若是直接運行 JUnit 測試用例會報錯,因此在此處增長了一個 @ClassRule,具體可參考
https://stackoverflow.com/questions/41121778/junit-rule-and-classrule
/** * Created by Xu on 2019/04/05. * * @author Xu */ public class RxImmediateSchedulerRule implements TestRule { private Scheduler immediate = new Scheduler() { @Override public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { // this prevents StackOverflowErrors when scheduling with a delay return super.scheduleDirect(run, 0, unit); } @Override public Worker createWorker() { return new ExecutorScheduler.ExecutorWorker(Runnable::run); } }; @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { RxJavaPlugins.setInitIoSchedulerHandler(scheduler -> immediate); RxJavaPlugins.setInitComputationSchedulerHandler(scheduler -> immediate); RxJavaPlugins.setInitNewThreadSchedulerHandler(scheduler -> immediate); RxJavaPlugins.setInitSingleSchedulerHandler(scheduler -> immediate); RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> immediate); try { base.evaluate(); } finally { RxJavaPlugins.reset(); RxAndroidPlugins.reset(); } } }; } }
至此,一個 Android 的單元測試用例編寫完成。經過 Android Studio 直接運行此單元測試用例,結果以下:
須要明白一個點:單元測試它只是測試一個方法單元,它不是測試一整個 APP 的功能流程,即單元測試不會涉及到數據庫或網絡等複雜的外部環境。好比說這裏咱們只測試到 NewsPresenter#getNews() 方法,並無測試 NewsFragment 的整個初始化到顯示的過程是否正常,數據是否有誤。(這樣的測試每每稱之爲集成測試)
若是要編寫一個新的本地 UI 測試用例,只需打開你想測試的 java 代碼文件,而後點擊類名 – ⇧⌘T(Windows:Ctrl+Shift+T)– 選擇要生成的方法 – 選擇 androidTest 文件夾,對應於本地 UI 測試 – 完成。
須要 Espresso 框架支持,因此在 build.gradle 中增長(注意是 androidTestImplementation):
androidTestImplementation "androidx.test:runner:1.1.0" androidTestImplementation "androidx.test:rules:1.1.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.0.2" androidTestImplementation "androidx.test.espresso:espresso-contrib:3.0.2" androidTestImplementation "androidx.test.espresso:espresso-intents:3.0.2" androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:3.0.2" androidTestImplementation "androidx.test.espresso:espresso-idling-resource:3.0.2"
筆者主要測試的代碼爲 NewsDetailActivity,主要功能是加載 intent 傳遞過來的新聞標題和新聞原文地址,而後在 Toolbar 中顯示新聞標題,在 Webview 中加載此新聞。
對應着,實際編寫測試代碼的時候,能夠構造一個測試用的 intent,在 intent 中加入須要的測試數據,而後啓動這個 activity,檢查數據是否正確便可。這裏咱們藉助 Espresso 框架,它有三個重要的組成部分:ViewMatchers(根據視圖 id 或其餘屬性匹配指定的 View),ViewActions(執行 View 的某些行爲,例如點擊事件),ViewAssertions(檢查 View 的某些狀態,例如指定 View 是否顯示在屏幕上)。
新建立的 UI 測試代碼以下:
/** * Created by Xu on 2019/04/09. */ @RunWith(AndroidJUnit4.class) @LargeTest public class NewsDetailActivityTest { @Rule public ActivityTestRule<NewsDetailActivity> newsDetailActivityActivityTestRule = new ActivityTestRule<>(NewsDetailActivity.class, true, false); @Before public void setUp() { Intent intent = new Intent(InstrumentationRegistry.getInstrumentation().getTargetContext(), NewsDetailActivity.class); intent.putExtra(Constants.NEWS_URL, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_URL); intent.putExtra(Constants.NEWS_IMG, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_IMG); intent.putExtra(Constants.NEWS_TITLE, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_TITLE); newsDetailActivityActivityTestRule.launchActivity(intent); IdlingRegistry.getInstance().register(newsDetailActivityActivityTestRule.getActivity().getCountingIdlingResource()); } @Test public void showNewsDetail() { onView(withId(R.id.toolbar)).check(matches(isDisplayed())); onView(withId(R.id.iv_news_detail_pic)).check(matches(isDisplayed())); onView(withId(R.id.clp_toolbar)).check(matches(isDisplayed())); onView(withId(R.id.clp_toolbar)).check(matches(withCollapsingToolbarLayoutText(is(TestConstants.NEWS_DETAIL_ACTIVITY_TEST_TITLE)))); } @After public void tearDown() { IdlingRegistry.getInstance().unregister(newsDetailActivityActivityTestRule.getActivity().getCountingIdlingResource()); } }
在其中:
@RunWith 註解能夠改變 JUnit 測試用例的的默認執行類,因爲這裏是須要 Android 環境且使用到 Espresso 框架,因此 @RunWith 選擇 AndroidJUnit4 類。@LargeTest 表示此測試用例會使用到外部文件系統或者網絡,而且運行時間大於 1000 ms。
什麼是 IdlingResource 呢?
一般來講,大多數 APP 在設計業務功能的過程當中,會有不少的異步任務,例如使用 Rxjava 發起網絡請求等,可是 Espresso 並不知道你的異步任務何時結束,若是單純使用 Thread.sleep() 等待異步回調的結果又過於「硬核」,因此須要藉助於 IdlingResource 這個類。
它須要在業務代碼中添加相關的邏輯。例如在 NewsDetailActivity 中,會接收到 intent 傳遞過來的新聞圖片地址,而後使用 Glide 異步加載此圖片,大體代碼以下:
public class NewsDetailActivity extends AppCompatActivity { @BindView(R.id.iv_news_detail_pic) private ImageView ivNewsDetailPic; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_news); // 省略部分代碼邏輯 // 開始發起異步操做,App開始進入忙碌狀態 EspressoIdlingResource.increment(); // 開始加載圖片 Glide.with(context).asBitmap().load(imgUrl).into(new GlideDrawableImageViewTarget(mAvatar) { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { super.onResourceReady(resource, transition); // 異步操做結束,將App設置成空閒狀態 if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) { EspressoIdlingResource.decrement(); } } }); } // 省略代碼 @VisibleForTesting public IdlingResource getCountingIdlingResource() { return EspressoIdlingResource.getIdlingResource(); } } public class EspressoIdlingResource { private static final String RESOURCE = "GLOBAL"; // Espresso 提供了一個實現好的 CountingIdlingResource 類 // 若是沒有特別需求的話,直接使用它便可 private static CountingIdlingResource countingIdlingResource = new CountingIdlingResource(RESOURCE); public static void increment() { countingIdlingResource.increment(); } public static void decrement() { countingIdlingResource.decrement(); } public static IdlingResource getIdlingResource() { if (countingIdlingResource == null) { countingIdlingResource = new CountingIdlingResource(RESOURCE); } return countingIdlingResource; } }
再加上咱們在測試代碼中聲明的 IdlingRegistry.getInstance().register() 和 IdlingRegistry.getInstance().unregister() 方法,根據 APP 是否處於忙碌狀態來判斷異步任務是否完成,這樣 Espresso 就能作到對異步任務進行相應的測試。
以鏈式代碼的形式編寫驗證測試結果的代碼,例如 onView(withId(R.id.toolbar)).check(matches(isDisplayed())); 意思就是獲取 id 爲 R.id.toolbar 的 view,檢查這個 view 是否正常顯示。
若是 Espresso 自帶的 View Matchers 不能知足需求的話,咱們也能夠自定義一個 matcher,例如 onView(withId(R.id.clp_toolbar)).check(matches(withCollapsingToolbarLayoutText(is(TestConstants.NEWS_DETAIL_ACTIVITY_TEST_TITLE)))); ,咱們獲取到的 view 是一個 CollapsingToolbarLayout,是一個特殊樣式的 Toolbar,咱們要檢查其中的標題是否與測試數據相匹配,咱們能夠編寫自定義的 Matcher:
public static Matcher<View> withCollapsingToolbarLayoutText(Matcher<String> stringMatcher) { return new BoundedMatcher<View, CollapsingToolbarLayout>(CollapsingToolbarLayout.class) { @Override public void describeTo(Description description) { description.appendText("with CollapsingToolbarLayout title: "); stringMatcher.describeTo(description); } @Override protected boolean matchesSafely(CollapsingToolbarLayout collapsingToolbarLayout) { return stringMatcher.matches(collapsingToolbarLayout.getTitle()); } }; }
這裏傳入一個 String 類型的匹配器(經過 is() 方法返回),返回一個 CollapsingToolbarLayout title 的 Matcher。
至此,一個 Android 的 UI 測試用例編寫完成。經過 Android Studio 直接運行此用例,結果以下:
本文主要從測試的兩個不一樣粒度:單元測試和 UI 測試入手,綜合參考 Google Sample 項目中的測試代碼,作一個初步實踐,分析編寫並運行相關的測試用例。
筆者認爲編寫 Android 的測試用例的大體流程以下: