Android Espresso——UI自動(dòng)化測(cè)試框架實(shí)踐

Android Espresso——UI自動(dòng)化測(cè)試框架實(shí)踐

Espresso是一個(gè)Google官方提供的Android應(yīng)用UI自動(dòng)化測(cè)試框架。Google希望,當(dāng)Android的開發(fā)者利用Espresso寫完測(cè)試用例后,能一邊看著測(cè)試用例自動(dòng)執(zhí)行,一邊享受一杯香醇Espresso(濃咖啡)。Espresso測(cè)試依賴Android設(shè)備或模擬器,模擬的是用戶的操作,運(yùn)行測(cè)試代碼后,應(yīng)用重新安裝在設(shè)備上,執(zhí)行測(cè)試代碼,執(zhí)行完即關(guān)閉應(yīng)用。

Espress有3個(gè)特點(diǎn):

  • 第一個(gè)收錄在Android Testing Supporting Library底下的測(cè)試框架
  • 模擬用戶的操作
  • 自動(dòng)等待,直到UI線程Idle,才會(huì)執(zhí)行測(cè)試代碼

1、添加依賴

  androidTestImplementation('com.android.support.test:runner:1.0.0')
  androidTestImplementation 'com.android.support.test:rules:1.0.0'
  androidTestImplementation 'org.hamcrest:hamcrest-library:1.0'
  androidTestImplementation ('com.android.support.test.espresso:espresso-core:3.0.0'){
      exclude(group:'com.google.code.findbugs')
  }
  androidTestImplementation ('com.android.support.test.espresso:espresso-intents:3.0.2')   {
      exclude(group:'com.google.code.findbugs')
  }
  androidTestImplementation ('com.android.support.test.espresso:espresso-contrib:2.2'){
      exclude group: 'com.android.support', module: 'appcompat'
      exclude group: 'com.android.support', module: 'support-v4'
      exclude module: 'recyclerview-v7'
      exclude(group:'com.google.code.findbugs')
  }

在android {}的defaultConfig{}中添加

android {

    defaultConfig {
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

2、寫用例

寫UI自動(dòng)化測(cè)試用例,歸結(jié)起來(lái)就是3步:

~定位View控件 ~操作View控件 ~校驗(yàn)View控件的狀態(tài) 對(duì)應(yīng)Espresso,就是以下3個(gè)方法的調(diào)用:

onView(ViewMatcher)
  .perform(ViewAction)
  .check(ViewAssertion);

其中,onView是用來(lái)定位View控件的,perform是操作控件的,check是校驗(yàn)View控件的狀態(tài)。他們各自都需要再傳入對(duì)應(yīng)的參數(shù)分別如下:

ViewMatcher,有withId、withText、withClassName等等方法來(lái)定位View控件 ViewAction,有click()、longClick()、pressBack()、swipeLeft()等等方法來(lái)操作View控件ViewAssertion,有isEnabled()、isLeftOf()、isChecked()等等方法來(lái)校驗(yàn)View控件狀態(tài) 這里有ViewMatcher、ViewAction、ViewAssertion的Cheat Sheet。

public class LoginActivityTest {

    //ActivityTestRule 提供了對(duì)單個(gè)Activity進(jìn)行測(cè)試的方法,可以通過其獲取activity中的元素。
    @Rule
    public ActivityTestRule<ActivityRegisterLoginAccountPsw> activityTestRule = new ActivityTestRule<>(ActivityRegisterLoginAccountPsw.class);

    //測(cè)試方法必須聲明為public
    @Test
    public void testLogin() {
        EditText account = activityTestRule.getActivity().findViewById(R.id.et_account);
        if (account.getText().length() == 0) {
            onView(withId(R.id.et_account)).perform(typeText("ordertest102"));
        }
        onView(withId(R.id.et_psw)).perform(typeText("111111"));
        //  onView(withId(R.id.tv_confirm)).perform(click());

        //  onView(withText("Masuk")).perform(click());

        onView(allOf(withId(R.id.tv_confirm), withText("Masuk"))).perform(click());

        IdlingRegistry.getInstance().register(new IdleLoginRequest(3000));
        onView(withId(R.id.et_psw)).check(matches(isDisplayed()));
    }
}

3、IdlingResource

應(yīng)用開發(fā)中很常見的一個(gè)場(chǎng)景是,點(diǎn)擊某個(gè)按鈕,發(fā)起網(wǎng)絡(luò)請(qǐng)求,等請(qǐng)求回來(lái)后解析數(shù)據(jù),更新界面。或者開始沒有加載界面,只有一個(gè)ProgressDialog在轉(zhuǎn)圈,這時(shí)去定位View是定位不到的,Espresso針對(duì)這種測(cè)試場(chǎng)景,提供了原生的支持。下面是示例代碼,主要是重寫isIdleNow()方法,邏輯控制UI線程變Idle的時(shí)間。IdlingResource默認(rèn)的超時(shí)時(shí)間是26秒,延時(shí)時(shí)間不要超過這個(gè)值,否則espresso會(huì)拋出異常??梢酝ㄟ^IdingPolicies.setIdlingResourceTimeout()修改這個(gè)超時(shí)時(shí)間。

public class IdlingDelayResources implements IdlingResource {
    private boolean timesUp;
    private ResourceCallback mCallback;

    public IdlingDelayResources(int delayMillis) {
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                timesUp = true;
            }
        },delayMillis);
    }

    @Override
    public String getName() {
        return IdlingDelayResources.class.getSimpleName();
    }

    @Override
    public boolean isIdleNow() {
        if (timesUp && mCallback != null) {
            mCallback.onTransitionToIdle();
        }
        return timesUp;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
        mCallback = callback;
    }
}

使用方式需要注冊(cè)和反注冊(cè):

IdlingDelayResources idle = new IdlingDelayResources(1000);

IdlingRegistry.getInstance().register(idle);

IdlingRegistry.getInstance().unregister(idle);

注意:實(shí)際使用IdlingResource過程中發(fā)現(xiàn)在注冊(cè)IdlingResource后,必須對(duì)應(yīng)一個(gè)UI的操作,否則無(wú)效??梢岳斫鉃楸仨氂幸粋€(gè)事件等待主線程變空閑。另外,示例代碼只是簡(jiǎn)單地延時(shí)XX秒,并不可靠,需盡量實(shí)現(xiàn)在異步任務(wù)確定已經(jīng)完成時(shí),將isIdleNow方法返回true。

4、自定義Matcher

如果espresso及hamcrest提供的諸多matcher依然不符合你的需求,可以自定義Matcher進(jìn)行view的匹配,例如,ListView中item的匹配,Adapter并不是簡(jiǎn)單的ArrayAdapter時(shí),需要自己定義match規(guī)則:

onData(allOf(is(instanceOf(EntityAdrSlv.Data.class)),withAddress("Kuta"))).inAdapterView(withId(R.id.acty_address_list_lv)).perform(click());

 public static Matcher<Object> withAddress(final String address) {
        return new BoundedMatcher<Object, EntityAdrSlv.Data>(EntityAdrSlv.Data.class) {
            @Override
            protected boolean matchesSafely(EntityAdrSlv.Data adr) {
                return address.equals(adr.f7);
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("with address: " + address);
            }
        };
    }

在測(cè)試類中寫一個(gè)靜態(tài)內(nèi)部類,返回類型為Matcher,內(nèi)部實(shí)現(xiàn)是返回一個(gè)TypeSafeMatcher接口或者BoundedMatcher抽象類并實(shí)現(xiàn)兩個(gè)方法。方法matchesSafely(View view)是關(guān)鍵的代碼,返回true代表匹配成功,返回false代表匹配失敗。describeTo()方法是在匹配失敗時(shí)的提示信息。

5、View匹配失敗

如果沒有匹配到你想要的View,測(cè)試結(jié)果會(huì)拋出NoMatchingViewException,并將日志輸出在控制臺(tái),整個(gè)View的樹結(jié)構(gòu)會(huì)被打印出來(lái),類似下面:

1563853003404.png

從上面的View結(jié)構(gòu)信息中基本可以發(fā)現(xiàn)問題在哪。常見的問題:

1、匹配到兩個(gè)或兩個(gè)以上相同的View,這時(shí)可以自定義Matcher,或者用allOf()來(lái)多條件匹配。

2、當(dāng)前View樹并不是你想的那個(gè)View,例如開始顯示的是一個(gè)ProgressDialog加載框,這時(shí)你去匹配的樹結(jié)構(gòu)是這個(gè)ProgressDialog的樹結(jié)構(gòu)。

3、View還沒有渲染出來(lái),雖然你寫了IdlingResource去延時(shí),但可能在代碼邏輯上界面應(yīng)該顯示了,實(shí)際上還沒有。所以IdlingResource判斷UI線程為Idle的時(shí)機(jī)很重要。

6、權(quán)限怎樣跳過

如果在測(cè)試過程中出現(xiàn)權(quán)限彈窗,此時(shí)使用onView方法匹配一個(gè)控件是無(wú)法匹配到的,因?yàn)榇藭r(shí)最頂層的View樹結(jié)構(gòu)對(duì)應(yīng)的是Dialog所在的Window。那么如何跳過權(quán)限彈窗呢?

1、官方提供了GrantPermissionRule來(lái)為測(cè)試代碼直接獲取權(quán)限,使用方式如下:

  @Rule
  public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION);

注意,@Rule注解標(biāo)識(shí)的變量必須為public類型,grant()方法的參數(shù)是一個(gè)String...類型的數(shù)組,可以傳入多個(gè)權(quán)限類型。

2、配合UIAutomator來(lái)模擬點(diǎn)擊彈窗。首先,添加依賴:

androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'

此時(shí)運(yùn)行測(cè)試代碼,如果你的minSdk<18,會(huì)編譯失敗,因?yàn)閁iAutomator最小支持的sdk版本是18。但通常我們不會(huì)因此就調(diào)整項(xiàng)目的minSdk,解決方案是在androidTest下新建一個(gè)AndroidManifest.xml

<manifest
    package="${applicationId}.test"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-sdk tools:overrideLibrary="android.support.test.uiautomator.v18"/>
</manifest>

這時(shí)就可以運(yùn)行成功了。那么怎樣模擬點(diǎn)擊權(quán)限彈窗上的允許按鈕呢?

public class PermissionGranter {

    private static final int PERMISSIONS_DIALOG_DELAY = 3000;
    private static final int GRANT_BUTTON_INDEX = 1;

    public static void allowPermissionsIfNeeded(String permissionNeeded) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasNeededPermission(permissionNeeded)) {
                sleep(PERMISSIONS_DIALOG_DELAY);
                UiDevice device = UiDevice.getInstance(getInstrumentation());
                UiObject allowPermissions = device.findObject(new UiSelector()
                        .clickable(true)
                        .checkable(false)
                        .index(GRANT_BUTTON_INDEX));
                if (allowPermissions.exists()) {
                    allowPermissions.click();
                }
            }
        } catch (UiObjectNotFoundException e) {
            System.out.println("There is no permissions dialog to interact with");
        }
    }

    private static boolean hasNeededPermission(String permissionNeeded) {
        Context context = InstrumentationRegistry.getTargetContext();
        int permissionStatus = ContextCompat.checkSelfPermission(context, permissionNeeded);
        return permissionStatus == PackageManager.PERMISSION_GRANTED;
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException("Cannot execute Thread.sleep()");
        }
    }
}

切記要在合適的時(shí)機(jī)調(diào)用上面的allowPermissionsIfNeeded方法,比如要在會(huì)觸發(fā)權(quán)限申請(qǐng)的頁(yè)面和操作后再去調(diào)用。

7、經(jīng)驗(yàn)分享

1、如何串聯(lián)整個(gè)應(yīng)用測(cè)試,目前已知通過gradle cAT命令執(zhí)行測(cè)試代碼默認(rèn)會(huì)執(zhí)行全部測(cè)試方法,但實(shí)際運(yùn)行起來(lái)每個(gè)方法之間并不是連貫的,每一個(gè)方法執(zhí)行完,頁(yè)面都會(huì)關(guān)閉,下一個(gè)用例開始時(shí)再打開。除非activityRule設(shè)置為應(yīng)用默認(rèn)啟動(dòng)的activity,通過模擬用戶操作到達(dá)待測(cè)Activity,個(gè)人覺得得不償失,很容易出錯(cuò),還是尊重google的設(shè)計(jì),沒必要完全連續(xù)的測(cè)試整個(gè)流程。

2、可能需要為了測(cè)試而去增加業(yè)務(wù)代碼,例如實(shí)在無(wú)法匹配到view的情況。

3、測(cè)試前建議關(guān)閉所有窗口動(dòng)畫,窗口動(dòng)畫、鍵盤、懸浮通知等都可能導(dǎo)致匹配不到view,導(dǎo)致測(cè)試不通過。

4、Android SDK提供了uiautomatorviewer.bat小工具,來(lái)幫你獲取當(dāng)前頁(yè)面下面的View信息,很好用,在你沒有項(xiàng)目代碼或者不想去找id的時(shí)候,它就派的上用場(chǎng)了。


1566894357168.png

5、通常官方提供的matcher不能夠完全滿足我們的需求,所以自定義matcher很有必要學(xué)習(xí)一下。目前我們項(xiàng)目中已經(jīng)提供了多重matcher,以及由matcher衍生出的工具類。

6、這方面國(guó)內(nèi)的資料比較少,善用StackOverflow和Google。

8、AppNotIdleException

當(dāng)有任務(wù)阻塞主線程時(shí),espresso會(huì)拋出這個(gè)異常。Espresso IdlePolicy默認(rèn)判斷主線程是否空閑的超時(shí)時(shí)間是60秒,可以通過IdlingPolicies.setMasterPolicyTimeout()方法修改超時(shí)時(shí)間。用下面的方法打印出當(dāng)前阻塞UI線程的任務(wù):

 public static void dumpThreads() {
        int activeCount = Thread.activeCount();
        Thread[] threads = new Thread[activeCount];
        Thread.enumerate(threads);
        for (Thread thread : threads) {
            if("RUNNABLE".equals(thread.getState().toString()) && thread.getName().startsWith("AsyncTask")) {
                LogUtils.e("EspressoTest", thread.getName() + ": " + thread.getState() + "=" + "RUNNABLE".equals(thread.getState().toString()));
                for (StackTraceElement stackTraceElement : thread.getStackTrace()) {
                    LogUtils.e("EspressoTest", "StackTrace:" + stackTraceElement);
                }
            }
        }
    }

在我們的項(xiàng)目中是因?yàn)橐蕾嚵薴acebook sdk,facebook sdk會(huì)在app啟動(dòng)時(shí)初始化,其中有兩個(gè)同步的網(wǎng)絡(luò)請(qǐng)求阻塞在那里,目前最有效的解決方案就是使用“梯子”,你們懂的~
9、測(cè)試報(bào)告
在Android Studio中打開Terminal命令窗口,輸入gradle connectedAndroidTest(簡(jiǎn)寫為gradle cAT)命令,會(huì)自動(dòng)執(zhí)行所有測(cè)試方法,并生成測(cè)試報(bào)告,存放在app/build/reports中。使用gradle命令測(cè)試完會(huì)卸載掉應(yīng)用。

1564368982327.png

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

相關(guān)閱讀更多精彩內(nèi)容

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