• <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>
  • Android 單元測試和 UI 測試初步實踐

    發表于:2019-07-02來源:Xu的博客作者:Xu的博客點擊數: 標簽:
    對于大多數 Android 商業項目,基本都是處于高速迭代的開發階段,這個階段不僅僅是對項目的開發效率,也對項目的產品質量提出了更高的要求。通常大型項目都是通過黑盒測試等方式

    對于大多數 Android 商業項目,基本都是處于高速迭代的開發階段,這個階段不僅僅是對項目的開發效率,也對項目的產品質量提出了更高的要求。

    通常大型項目都是通過黑盒測試等方式來提供質量相關的保障,但同時筆者認為也需要 Android 端的單元測試以及能自動在 Android 平臺上運行的 UI 測試,這幾種測試有以下幾個優勢:

    • 更早發現代碼中存在的 bug 等問題,提前 fix bug;
    • 更好地設計:在進行項目重構的時候,保證重構的新代碼能正確運行,這樣就能在業務不斷迭代的同時,更好地保障產品質量。

    Android 測試代碼位置

    在 Android Studio 中新建新的項目時,它已自動為兩種測試類型創建了對應的代碼目錄:

    • 單元測試用例:位于 module-name/src/test/java 目錄下,只依賴 JVM 環境而不需要 Android 環境
    • InstrumentTest 測試/ UI 測試用例:位于 module-name/src/androidTest/java 目錄下,在 Android 環境下才能運行

    接下來,筆者將嘗試為自己的項目(基于 MVP 架構開發)補充相應的單元測試用例和 UI 測試用例,來初步實踐下如何在 Android 平臺編寫和運行相關的測試用例。

    Android 單元測試實踐

    創建新用例

    如果需要編寫一個新的本地單元測試用例,只需打開你想測試的 java 代碼文件,然后點擊類名 – ??T(Windows:Ctrl+Shift+T)– 選擇要生成的方法 – 選擇 test 文件夾,對應于本地單元測試 – 完成。

    增加依賴庫

    需要 JUnit 和 Mockito 框架支持,所以在 build.gradle 中增加:

    testImplementation "junit:junit:4.12"
    testImplementation "org.mockito:mockito-core:2.7.1"
    

    編寫測試代碼

    一般來說,編寫一段測試代碼需要三個步驟:

    • 環境初始化
    • 執行操作
    • 驗證結果正確性

    筆者主要測試的是 MVP 架構中 P 層的代碼。在筆者的項目中,P 層是通過 Dagger2 機制,注入一個 DataManager,也就是數據獲取源。同時也需要一個 V 層的代理,這樣在 P 層通過數據源獲取數據之后,就能將數據交給 V 層,由 V 層去展示。

    代碼調用大致邏輯如下:

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

    在其中:

    1. 在代碼開頭,聲明了一個 @ClassRule;

    什么是 @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();
                    }
                }
            };
        }
    }
    
    1. 采用 Mockito 框架 mock 一個測試用的 DataManager 和 V 層代理 NewsContract.View。所謂的 mock 就是創建一個類的虛假的對象,在測試環境中,用來替換掉真實的對象,以達到驗證對象方法調用情況,或是指定這個對象的某些方法返回特定的值等;
    2. @Before 注解的方法會在執行測試用例之前執行,這里做一個初始化的操作,主要是 Mockito 框架的初始化及 presenter 的初始化;@After 注解的方法會在執行測試用例之后執行,這里做一個 presenter 的 detach() 操作,防止出現內存泄露等問題;
    3. @Test 注解的方法是實際執行的測試方法。這里根據之前的業務代碼邏輯:
    • 環境初始化:由于 NewsPresenter 的業務邏輯中是需要 DataManager 返回一個 NewsResultBean 實例才能進行后續的操作,而 mock 的話只能返回一個空對象,所以在代碼前兩行筆者通過 Mockito 的 when() 方法,在程序調用 DataManager#getNews() 方法時返回一個空的 NewsResultBean 實例。
    • 執行操作:執行 P 層的 NewsPresenter#getNews()。在業務邏輯中,執行此方法之后,會先調用 DataManager#getNews(),然后將數據交給 V 層的代理。
    • 驗證結果正確性:一般來說,我們要驗證一個方法執行結果是否正確,最簡單的方法的就是看執行完的方法輸出是否與預期輸出相一致。但在這里,NewsPresenter#getNews() 為一個 void 方法,沒有返回值,那么該怎么驗證呢?其實這個方法也是有輸出的,輸出就是:調用了 DataManager#getNews() 方法,獲取到數據后調用 NewsContract.View#showNews(news) 顯示數據。所以這里主要驗證的是 DataManager#getNews() 和 NewsContract.View#showProgress(),NewsContract.View#showNews(news) 和 NewsContract.View#hideProgress() 這三個方法是否有被調用到,這里運用到 Mockito 的 verify() 方法。

    至此,一個 Android 的單元測試用例編寫完成。通過 Android Studio 直接運行此單元測試用例,結果如下:

    需要明白一個點:單元測試它只是測試一個方法單元,它不是測試一整個 APP 的功能流程,即單元測試不會涉及到數據庫或網絡等復雜的外部環境。比如說這里我們只測試到 NewsPresenter#getNews() 方法,并沒有測試 NewsFragment 的整個初始化到顯示的過程是否正常,數據是否有誤。(這樣的測試往往稱之為集成測試)

    Android UI 測試實踐

    創建新用例

    如果要編寫一個新的本地 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());
        }
    }
    

    在其中:

    1. 在類聲明的開頭,添加了兩個注解 @RunWith(AndroidJUnit4.class) 和 @LargeTest;

    @RunWith 注解可以改變 JUnit 測試用例的的默認執行類,由于這里是需要 Android 環境且使用到 Espresso 框架,所以 @RunWith 選擇 AndroidJUnit4 類。@LargeTest 表示此測試用例會使用到外部文件系統或者網絡,并且運行時間大于 1000 ms。

    1. 聲明了一個變量 newsDetailActivityActivityTestRule 并用 @Rule 注解,newsDetailActivityActivityTestRule 是 ActivityTestRule 的實例化對象。ActivityTestRule 主要用來測試單個 Activity,這個 Activity 將在 @Test 和 @Before 前啟動。它其中包含一些基礎功能,例如啟動 Activity,獲取當前 Activity 實例等;
    2. 同樣的,這里 @Before 注解的方法會在執行測試用例之前執行,這里構造一個測試用 intent,最后通過 newsDetailActivityActivityTestRule#launchActivity(intent) 方法啟動待測試 Activity,并做一個 IdlingResource 的綁定;@After 注解的方法會在執行測試用例之后執行,這里做一個 IdlingResource 的解綁操作;

    什么是 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 就能做到對異步任務進行相應的測試。

    1. @Test 注解的方法是實際執行的測試方法。這里根據之前的業務代碼邏輯:
    • 環境初始化:模擬了測試的 intent 數據
    • 執行操作:加載 intent 傳遞過來的數據
    • 驗證結果正確性:檢查對應的 UI 樣式是否正常顯示測試數據,這里主要利用 Espresso 的 幾個重要的 API:
      • onView():獲得視圖 view,這里通過 withId() 方法搜索,即根據 id 來獲取對應的 view
      • check():檢驗視圖 view,可以檢查視圖文本是否匹配或者視圖是否顯示等,主要依靠 match() 方法返回對應的匹配類,Espresso 也自帶很多已封裝好的 View Matchers 供使用

    以鏈式代碼的形式編寫驗證測試結果的代碼,例如 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 的測試用例的大致流程如下:

    1. 確定需要編寫的測試用例粒度;
    2. 分析針對需要測試的頁面,提取出較為重要且簡短的業務代碼邏輯;
    3. 根據這些邏輯,通過三步走(初始化–執行–驗證)方法來設計測試用例,這里的業務邏輯不僅僅是指業務需求,還包括其他需要維護的業務或公共代碼邏輯;
    4. 在做單元測試時,個人認為測試的業務邏輯不需要跨很多頁面,在當前頁面執行即可,以免增加單元測試用例的維護成本;
    5. 單元測試用例并不能直接提升代碼質量,但能夠在進行項目重構的時候,保證重構的新代碼能正確運行,降低風險。

    原文轉自:https://dev-xu.cn/posts/1e6d7596.html

    老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月

  • <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>