美文网首页
单元测试--Espresso测试框架的使用

单元测试--Espresso测试框架的使用

作者: lisx_ | 来源:发表于2020-04-09 11:36 被阅读0次

    原文链接:川峰-Espresso测试框架的使用

    Espresso是Google官方提供并推荐的Android测试库,它是一个AndroidJunit单元测试库,用于Android仪器化测试,即需要运行到设备或模拟器上进行测试。Espresso是意大利语“咖啡”的意思,它的最大的优势是可以实现UI自动化测试,设计者的意图是想实现“喝杯咖啡的功夫”就可以等待自动测试完成。通常我们需要手动点击测试的UI功能,利用这个库可以自动为你实现。

    添加依赖:

    dependencies {
        androidTestImplementation 'com.android.support.test:runner:1.0.2'
        androidTestImplementation 'com.android.support.test:rules:1.0.2'
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
        androidTestImplementation "com.android.support.test.espresso:espresso-contrib:3.0.2"
        androidTestImplementation "com.android.support.test.espresso:espresso-idling-resource:3.0.2"
        androidTestImplementation "com.android.support.test.espresso:espresso-intents:3.0.2"
    }
    
    android {
        defaultConfig {
            ....
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }
    

    目前使用AS创建项目的时候,会自动为你添加Espresso的依赖。

    官方Doc入口:Espresso basics

    Espresso 由以下三个基础部分组成:

    • ViewMatchers 在当前View层级去匹配指定的View
    • ViewActions 执行Views的某些行为
    • ViewAssertions 检查Views的某些状态

    获取View

    //根据id匹配
    onView(withId(R.id.my_view))
    //根据文本匹配
    onView(withText("Hello World!"))
    

    执行View的行为

    //点击
    onView(...).perform(click());
    //输入文本
    onView(...).perform(typeText("Hello World"), closeSoftKeyboard());
    //滑动(使屏幕外的view显示) 点击
    onView(...).perform(scrollTo(), click());
    //清除文本
    onView(...).perform(clearText());
    

    一些方法含义:

    方法名 含义
    click() 点击view
    clearText() 清除文本内容
    swipeLeft() 从右往左滑
    swipeRight() 从左往右滑
    swipeDown() 从上往下滑
    swipeUp() 从下往上滑
    click() 点击view
    closeSoftKeyboard() 关闭软键盘
    pressBack() 按下物理返回键
    doubleClick() 双击
    longClick() 长按
    scrollTo() 滚动
    replaceText() 替换文本
    openLinkWithText() 打开指定超链
    typeText() 输入文本

    检验View内容

    //检验View的本文内容是否匹配“Hello World!”
    onView(...).check(matches(withText("Hello World!")));
    //检验View的内容是否包含“Hello World!”
     onView(...).check(matches(withText(containsString("Hello World!"))));
    //检验View是否显示
    onView(...).check(matches(isDisplayed()));
    //检验View是否隐藏
    onView(...).check(matches(not(isDisplayed())));
    

    其中check中匹配的内容可以嵌套任何Matchers类的函数。

    简单例子

    下面建一个模拟登陆的页面,xml布局:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:layout_margin="30dp"
                  android:orientation="vertical"
        >
         <EditText
             android:id="@+id/edit_name"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:inputType="text"
             android:maxLines="1"
             android:hint="请输入用户名"
             android:textSize="16sp"
             android:textColor="@android:color/black" />
    
         <EditText
             android:id="@+id/edit_password"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:inputType="textVisiblePassword"
             android:maxLines="1"
             android:hint="请输入密码"
             android:textSize="16sp"
             android:textColor="@android:color/black" />
    
         <Button
             android:id="@+id/btn_login"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginTop="20dp"
             android:text="登录"
             android:textSize="17sp"
             android:textColor="@android:color/black"
             />
    </LinearLayout>
    

    Activity代码:

    public class LoginActivity extends Activity implements View.OnClickListener {
    
        private EditText mNameEdit;
        private EditText mPasswordEdit;
        private Button mLoginBtn;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
            initView();
        }
    
        private void initView() {
            mNameEdit = (EditText) findViewById(R.id.edit_name);
            mPasswordEdit = (EditText) findViewById(R.id.edit_password);
            mLoginBtn = (Button) findViewById(R.id.btn_login);
            mLoginBtn.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_login:
                    login();
                    break;
                default:
                    break;
            }
        }
    
        private void login() {
            if (TextUtils.isEmpty(mNameEdit.getText().toString())) {
                Toast.makeText(this, "用户名为空", Toast.LENGTH_SHORT).show();
                return;
            }
            if (TextUtils.isEmpty(mPasswordEdit.getText().toString())) {
                Toast.makeText(this, "密码为空", Toast.LENGTH_SHORT).show();
                return;
            }
            if (mPasswordEdit.getText().length() < 6) {
                Toast.makeText(this, "密码长度小于6", Toast.LENGTH_SHORT).show();
                return;
            }
            mLoginBtn.setText("登录成功");
            Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
        }
    
    }
    

    测试类代码:

    @RunWith(AndroidJUnit4.class)
    @LargeTest//允许测试需要较大消耗
    public class LoginActivityTest {
    
        //指定测试的目标Activity页面
        @Rule
        public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);
    
        @Test
        public void testLogin() {
            //验证是否显示
            onView(withId(R.id.btn_login)).check(matches(isDisplayed()));
    
            //不输入任何内容,直接点击登录按钮
            onView(withId(R.id.btn_login)).perform(click());
            //onView(allOf(withId(R.id.btn_login), isDisplayed())).perform(click());
    
            //只输入用户名
            onView(withId(R.id.edit_name)).perform(typeText("admin"), closeSoftKeyboard());
            onView(withId(R.id.btn_login)).perform(click());
            onView(withId(R.id.edit_name)).perform(clearText());
    
            //同时输入用户名和密码,但是密码格式不正确
            onView(withId(R.id.edit_name)).perform(typeText("admin"));
            onView(withId(R.id.edit_password)).perform(typeText("123"), closeSoftKeyboard());
            onView(withId(R.id.btn_login)).perform(click());
            onView(withId(R.id.edit_name)).perform(clearText());
            onView(withId(R.id.edit_password)).perform(clearText());
    
            //输入正确的用户名和密码
            onView(withId(R.id.edit_name)).perform(typeText("admin"));
            onView(withId(R.id.edit_password)).perform(typeText("123456"), closeSoftKeyboard());
            onView(withId(R.id.btn_login)).perform(click());
    
            //验证内容
            onView(withId(R.id.btn_login)).check(matches(withText("登录成功")));
            onView(withId(R.id.edit_name)).check(matches(withText("admin")));
            onView(withId(R.id.edit_password)).check(matches(withText("123456")));
        }
    
    }
    

    运行测试

    当我们右键运行测试方法时,AS会为我们生成两个apk进行安装:


    当这两个apk安装到手机上以后,Espresso会根据测试用例自动打开LoginActivity, 然后执行输入点击等一系列操作,整个过程是全自动的,不需要你去手动操作。


    当运行完毕,会关闭LoginActivity,如果没有出错,控制台会显示测试通过:


    这里有一个地方需要注意的是,如果手机用的是搜狗输入法最好先切换成系统输入法,必须保证只有全键盘的输入法,否则输入的时候会有问题,比如搜狗的输入“admin”时,默认第一个字母会大写, 结果变成“Admin”, 导致测试结果不匹配。

    如果你想输入中文目前还做不到通过键盘的方式输入(因为Espresso不知道哪些按键可以输出你要的文字),所以中文只能用replaceText的方法:

    onView(withId(R.id.edit_name)).perform(replaceText("小明"));

    建议所有的输入都使用replaceText()的方式,这样就不用担心键盘输入法的问题了。

    验证Toast

    上面测试代码如果要验证Toast是否弹出,可以这么写:

    @Test
        public void testLogin() throws Exception {
            //不输入任何内容,直接点击登录按钮
            onView(withId(R.id.btn_login)).perform(click());
            //验证是否弹出文本为"用户名为空"的Toast
            onView(withText("用户名为空"))
                    .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView()))))
                    .check(matches(isDisplayed()));
    
            Thread.sleep(1000);
    
            //只输入用户名
            onView(withId(R.id.edit_name)).perform(typeText("admin"), closeSoftKeyboard());
            onView(withId(R.id.btn_login)).perform(click());
            //验证"密码为空"的Toast
            onView(withText("密码为空"))
                    .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView()))))
                    .check(matches(isDisplayed()));
            onView(withId(R.id.edit_name)).perform(clearText());
    
            Thread.sleep(1000);
    
            //同时输入用户名和密码,但是密码格式不正确
            onView(withId(R.id.edit_name)).perform(typeText("admin"));
            onView(withId(R.id.edit_password)).perform(typeText("123"), closeSoftKeyboard());
            onView(withId(R.id.btn_login)).perform(click());
            //验证"密码长度小于6"的Toast
            onView(withText("密码长度小于6"))
                    .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView()))))
                    .check(matches(isDisplayed()));
            //........
        }
    

    因为Toast的显示需要一定的时间,所以如果在一个测试方法中连续的测试Toast这里需要调用Thread.sleep(1000)休眠等待一段时间再继续,否则会检测不到。当然最好的做法是将每一个测试用例放到一个单独的单元测试方法中,这样可以不用等待。

    验证Dialog

    验证Dialog的方法跟Toast其实一样的。我们在Activity中back键按下的时候弹出一个提醒弹窗:

        @Override
        public void onBackPressed() {
            new AlertDialog.Builder(this)
                    .setTitle("提示")
                    .setMessage("确认退出应用吗")
                    .setPositiveButton("确认", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            dialogInterface.dismiss();
                            finish();
                        }
                    }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            dialogInterface.dismiss();
                        }
                    }).create().show();
        }
    

    测试代码:

        @Test
        public void testDialog() throws Exception {
            //按下返回键
            pressBack();
            //验证提示弹窗是否弹出
            onView(withText(containsString("确认退出应用吗")))
                    .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView()))))
                    .check(matches(isDisplayed()));
            //点击弹窗的确认按钮
            onView(withText("确认"))
                    .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView()))))
                    .perform(click());
            Assert.assertTrue(mActivityTestRule.getActivity().isFinishing());
        }
    

    验证目标Intent

    修改代码将上面LoginActivity中点击登录按钮的时候跳转到另一个Activity:

        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_login:
                    //login();
                    Intent intent = new Intent(this, HomeActivity.class);
                    intent.putExtra("id", "123");
                    startActivity(intent);
                    break;
                default:
                    break;
            }
        }
    

    测试代码:

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class LoginActivityTest2 {
    
        @Rule
        public IntentsTestRule mIntentTestRule = new IntentsTestRule<>(LoginActivity.class);
    
        @Before
        public void setUp() {
            Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, null);
            //当启动目标的Activity时,设置一个模拟的ActivityResult
            intending(allOf(
                    toPackage(InstrumentationRegistry.getTargetContext().getPackageName()),
                    hasComponent(hasShortClassName("HomeActivity"))
            )).respondWith(result);
        }
    
        @Test
        public void testJump() throws Exception {
            onView(withId(R.id.btn_login)).perform(click());
            //目标Intent是否启动了
            intended(allOf(
                    toPackage(InstrumentationRegistry.getTargetContext().getPackageName()),
                    hasComponent(HomeActivity.class.getName()),
                    hasExtra("id", "123")
            ));
        }
    }
    

    这里需要把ActivityTestRule换成IntentsTestRule,IntentsTestRule本身是继承ActivityTestRule的。intending 表示触发一个Intent的时候,类似于Mockito.when语法,你可以通过intending(matcher).thenRespond(myResponse)的方式来设置当触发某个intent的时候模拟一个返回值,intended表示是否已经触发某个Intent, 类似于Mockito.verify(mock, times(1))。

    其中Intent的匹配项可以通过IntentMatchers类提供的方法:


    更多Intent的匹配请参考官方:https://developer.android.google.cn/training/testing/espresso/intents

    访问Activity

    获取ActivityTestRule指定的Activity实例:

        @Test
        public void test() throws Exception {
            LoginActivity activity = mActivityTestRule.getActivity();
            activity.login();
        }
    

    获取前台Activity实例(如启动另一个Activity):

        @Test
        public void navigate() {
            onView(withId(R.id.btn_login)).perform(click());
            Activity activity = getActivityInstance();
            assertTrue(activity instanceof HomeActivity);
            // do more
        }
    
        public Activity getActivityInstance() {
            final Activity[] activity = new Activity[1];
            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
                @Override
                public void run() {
                    Activity currentActivity = null;
                    Collection resumedActivities =
                            ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED);
                    if (resumedActivities.iterator().hasNext()) {
                        currentActivity = (Activity) resumedActivities.iterator().next();
                        activity[0] = currentActivity;
                    }
                }
            });
            return activity[0];
        }
    
    

    手动启动Activity

    默认测试方法是会直接启动Activity的,也可以选择自己启动Activity并通过Intent传值

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class LoginActivityTest3 {
    
        @Rule
        public ActivityTestRule mRule = new ActivityTestRule<>(LoginActivity.class, true, false);
    
        @Test
        public void start() throws Exception {
            Intent intent = new Intent();
            intent.putExtra("name", "admin");
            mRule.launchActivity(intent);
            onView(withId(R.id.edit_name)).check(matches(withText("admin")));
        }
    
    }
    

    提前注入Activity的依赖

    有时Activity在创建之前会涉及到一些第三方库的依赖,这时直接跑测试方法会报错。

       @Override
        protected void onCreate(Bundle savedInstanceState) {
            Log.e("AAA", "onCreate: " + ThirdLibrary.instance.name);
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
            initView();
            checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.CALL_PHONE);
        }
    

    通过ActivityTestRule可以在Activity启动之前或之后做一些事情,我们可以选择在Activity启动之前创建好这些依赖:

     @Rule
        public ActivityTestRule<LoginActivity> mActivityTestRule = new ActivityTestRule<LoginActivity>(LoginActivity.class){
    
            @Override
            protected void beforeActivityLaunched() {
                //在目标Activity启动之前执行,会在每个测试方法之前运行,包括@Before
                ThirdLibrary.instance = new ThirdLibrary();
            }
    
            @Override
            protected void afterActivityLaunched() {
                //在目标Activity启动之后执行,会在任意测试方法之前运行,包括@Before
            }
    
            @Override
            protected void afterActivityFinished() {
                //启动Activity结束以后执行,会在任意测试方法之后运行,包括@After
            }
        };
    

    这样在测试方法之前会先执行beforeActivityLaunched创建好Activity需要的依赖库。

    添加权限

    Espresso可以在运行测试方法前手动为应用授予需要的权限:

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class LoginActivityTest {
        public static String[] PERMISSONS_NEED = new String[] {
                Manifest.permission.CALL_PHONE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
        };
    
        @Rule
        public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant(PERMISSONS_NEED);
    
        @Test
        public void testPermission() throws Exception {
            LoginActivity activity = mActivityTestRule.getActivity();
            Assert.assertEquals(PackageManager.PERMISSION_GRANTED,
                    ContextCompat.checkSelfPermission(activity, PERMISSONS_NEED[0]));
            Assert.assertEquals(PackageManager.PERMISSION_GRANTED,
                    ContextCompat.checkSelfPermission(activity, PERMISSONS_NEED[1]));
        }
    }
    

    通过@Rule注解指定一个GrantPermissionRule即可。GrantPermissionRule指定目标页面需要的权限,在测试方法运行前会自动授权。

    测试View的位置

    直接看图:


    RecyclerView点击Item

    点击某个Item:

        @Test
        public void clickItem() {
            onView(withId(R.id.rv_person))
                    .perform(actionOnItemAtPosition(10, click()));
            //或者:
            onView(withId(R.id.rv_person))
                    .perform(actionOnItem(hasDescendant(withText("姓名10")), click()));
            //验证toast弹出
            onView(withText("姓名10"))
                    .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView()))))
                    .check(matches(isDisplayed()));
        }
    

    点击某个Item的子View,可以通过自定义ViewAction实现:

        @Test
        public void clickChildItem() {
            onView(withId(R.id.rv_person))
                    .perform(actionOnItemAtPosition(10, clickChildViewWithId(R.id.tv_name)));
            //验证toast弹出
            onView(withText("onItemChildClick"))
                    .inRoot(withDecorView(not(is(mActivityTestRule.getActivity().getWindow().getDecorView()))))
                    .check(matches(isDisplayed()));
        }
    
        public static ViewAction clickChildViewWithId(final int id) {
            return new ViewAction() {
                @Override
                public Matcher<View> getConstraints() {
                    return null;
                }
    
                @Override
                public String getDescription() {
                    return "Click on a child view with specified id.";
                }
    
                @Override
                public void perform(UiController uiController, View view) {
                    View v = view.findViewById(id);
                    v.performClick();
                }
            };
        }
    

    RecyclerViewActions类提供的一些可以操作RecyclerView的方法:

    方法名 含义
    scrollTo 滚动到匹配的view
    scrollToHolder 滚动到匹配的viewholder
    scrollToPosition 滚动到指定的position
    actionOnHolderItem 在匹配到的view holder中进行操作
    actionOnItem 在匹配到的item view上进行操作
    actionOnItemAtPosition 在指定位置的view上进行操作

    ListView点击Item

    假设ListView的Adapter中的Item的定义如下:

    public static class Item {
        private final int value;
        public Item(int value) {
            this.value = value;
        }
        public String toString() {
            return String.valueOf(value);
        }
    }
    

    点击某个item:

    @Test
    public void clickItem() {
        onData(withValue(27))
                .inAdapterView(withId(R.id.list))
                .perform(click());
        //Do the assertion here.
    }
    
    public static Matcher<Object> withValue(final int value) {
        return new BoundedMatcher<Object,
                MainActivity.Item>(MainActivity.Item.class) {
            @Override public void describeTo(Description description) {
                description.appendText("has value " + value);
            }
            @Override public boolean matchesSafely(
                    MainActivity.Item item) {
                return item.toString().equals(String.valueOf(value));
            }
        };
    }
    

    点击某个item的子View:

    onData(withItemContent("xxx")).onChildView(withId(R.id.tst)).perform(click());
    

    更多列表点击的介绍可以参考官方:
    https://developer.android.google.cn/training/testing/espresso/lists

    自定义Matcher

    匹配EditText的hint为例:

    /**
     * A custom matcher that checks the hint property of an {@link android.widget.EditText}. It
     * accepts either a {@link String} or a {@link org.hamcrest.Matcher}.
     */
    public class HintMatcher {
    
        static Matcher<View> withHint(final String substring) {
            return withHint(is(substring));
        }
    
        static Matcher<View> withHint(final Matcher<String> stringMatcher) {
            checkNotNull(stringMatcher);
            return new BoundedMatcher<View, EditText>(EditText.class) {
    
                @Override
                public boolean matchesSafely(EditText view) {
                    final CharSequence hint = view.getHint();
                    return hint != null && stringMatcher.matches(hint.toString());
                }
    
                @Override
                public void describeTo(Description description) {
                    description.appendText("with hint: ");
                    stringMatcher.describeTo(description);
                }
            };
        }
    }
    

    测试代码:

    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class HintMatchersTest {
        private static final String COFFEE_ENDING = "coffee?";
        private static final String COFFEE_INVALID_ENDING = "tea?";
        
        @Rule
        public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);
    
        /**
         * Uses a custom matcher {@link HintMatcher#withHint}, with a {@link String} as the argument.
         */
        @Test
        public void hint_isDisplayedInEditText() {
            String hintText = InstrumentationRegistry.getContext()
                    .getResources().getString(R.string.hint_edit_name);
            onView(withId(R.id.edit_name))
                    .check(matches(HintMatcher.withHint(hintText)));
        }
    
        /**
         * Same as above but using a {@link org.hamcrest.Matcher} as the argument.
         */
        @Test
        public void hint_endsWith() {
            // This check will probably fail if the app is localized and the language is changed. Avoid string literals in code!
            onView(withId(R.id.edit_name)).check(matches(HintMatcher.withHint(anyOf(
                    endsWith(COFFEE_ENDING), endsWith(COFFEE_INVALID_ENDING)))));
        }
    
    }
    

    IdlingResource的使用

    虽然单元测试不建议处理异步的操作,但是Espresso也提供了这样的支持,因为实际中还是会有很多地方会用到的,常见的场景有异步网络请求、异步IO数据操作等。Espresso提供的解决异步问题的方案就是IdlingResource。

    例如Activity启动后加载网络图片需要经过一段时间再更新ImageView显示:

    public class LoadImageActivity extends Activity  {
        private ImageView mImageView;
        private boolean mIsLoadFinished;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_load_image);
            mImageView = (ImageView) findViewById(R.id.iv_test);
    
            final String url = "http://pic21.photophoto.cn/20111019/0034034837110352_b.jpg";
            Glide.with(this)
                    .load(url)
                    .into(new SimpleTarget<Drawable>() {
                        @Override
                        public void onResourceReady(Drawable resource, Transition<? super Drawable> transition) {
                            mImageView.setImageDrawable(resource);
                            mImageView.setContentDescription(url);
                            mIsLoadFinished = true;
                        }
    
                        @Override
                        public void onLoadFailed(@Nullable Drawable errorDrawable) {
                            super.onLoadFailed(errorDrawable);
                            mIsLoadFinished = true;
                        }
                    });
        }
    
        public boolean isLoadFinished() {
            return mIsLoadFinished;
        }
    }
    

    这时测试用例验证必须等待加载完以后才能进行。我们需要定义一个IdlingResource接口的实现类:

    public class SimpleIdlingResource implements IdlingResource {
        private volatile ResourceCallback mCallback;
        private LoadImageActivity mLoadImageActivity;
    
        public SimpleIdlingResource(LoadImageActivity activity) {
            mLoadImageActivity = activity;
        }
    
        @Override
        public String getName() {
            return this.getClass().getName();
        }
    
        @Override
        public boolean isIdleNow() {
            if (mLoadImageActivity != null && mLoadImageActivity.isLoadFinished()) {
                if (mCallback != null) {
                    mCallback.onTransitionToIdle();
                }
                return true;
            }
            return false;
        }
    
        @Override
        public void registerIdleTransitionCallback(ResourceCallback callback) {
            mCallback = callback;
        }
    }
    
    

    IdlingResource接口的三个方法含义:

    • getName()方法返回的String是作为注册回调的Key,所以要确保唯一性
    • registerIdleTransitionCallback()的参数ResourceCallback会用做isIdleNow()时候的回调
    • isIdleNow()是否已经处于空闲状态,这里是通过查询Activity的一个标志位来判断的
      定义好IdlingResource接口的实现类之后,我们需要在测试类当中的测试方法执行之前和之后进行注册和反注册,我们可以在@Before方法中进行注册,而在@After方法中进行反注册:
    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class LoadImageActivityTest {
    
        @Rule
        public ActivityTestRule<LoadImageActivity> mActivityRule = new ActivityTestRule<>(LoadImageActivity.class);
    
        SimpleIdlingResource mIdlingResource;
    
        @Before
        public void setUp() throws Exception {
            LoadImageActivity activity = mActivityRule.getActivity();
            mIdlingResource = new SimpleIdlingResource(activity);
            IdlingRegistry.getInstance().register(mIdlingResource);
        }
    
        @After
        public void tearDown() throws Exception {
            IdlingRegistry.getInstance().unregister(mIdlingResource);
        }
    
        @Test
        public void loadImage() throws Exception {
            String url = "http://pic21.photophoto.cn/20111019/0034034837110352_b.jpg";
            onView(withId(R.id.iv_test)).check(matches(withContentDescription(url)));
        }
    }
    

    这时运行测试方法loadImage(),Espresso会首先启动LoadImageActivity,然后一直等待SimpleIdlingResource中的状态变为idle时,才会去真正执行loadImage()测试方法里的代码,达到了同步的效果目的。
    同样,其他的异步操作都是类似的处理。

    Espresso UI Recorder

    Espresso在Run菜单中提供了一个Record Espresso Test功能,选择之后可以将用户的操作记录下来并转成测试代码。


    选择之后,会弹出一个记录面板:


    image

    接下来你在手机上针对应用的每一步操作会被记录下来,最后点击Ok就会自动生成刚才操作的case代码了。试了一下这个功能的想法还是很好的,可是操作起来太卡了,不太实用。

    WebView的支持

    为方便测试Espresso专门为WebView提供了一个支持库,具体参考官方介绍:Espresso Web

    多进程的支持

    单元测试很少用,直接看官方的介绍: Multiprocess Espresso

    Accessibility的支持

    直接看官方的介绍:Accessibility checking

    More Espresso Demo

    更多Espresso的Demo可以参考官方的介绍:Additional Resources for Espresso 基本上demo都是在Github上面的, 本文中找不到的例子可以到里面找找看。

    Espresso备忘清单

    最后再来一张Espresso备忘清单,方便速查:


    Espresso踩坑

    目前官方Espresso最新的版本是3.1.0,支持androidX(API 28 Android 9.0), 我在测试的时候由于AS还没有升级(用的是3.1.4的版本),所以新建项目的时候,默认添加的还是3.0.2的版本,如果compileSdkVersion和targetSdkVersion都是27,并且所有的support依赖库都是27.1.1版本的则没有问题,但是有一些第三方库还是support 26.1.0的,所以会出现下面的问题:


    这个问题真的很难搞,折腾了好久,采用下面的方法,强行指定support版本库为26.1.0:

    configurations.all {
        resolutionStrategy.force 'com.android.support:design:26.1.0'
        resolutionStrategy.force 'com.android.support:support-annotations:26.1.0'
        resolutionStrategy.force 'com.android.support:recyclerview-v7:26.1.0'
        resolutionStrategy.force 'com.android.support:support-v4:26.1.0'
        resolutionStrategy.force 'com.android.support:appcompat-v7:26.1.0'
        resolutionStrategy.force 'com.android.support:cardview-v7:26.1.0'
        resolutionStrategy.force 'com.android.support:support-core-utils:26.1.0'
        resolutionStrategy.force 'com.android.support:support-compat:26.1.0'
    }
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'com.android.support.test:runner:1.0.2'
        androidTestImplementation 'com.android.support.test:rules:1.0.2'
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
        androidTestImplementation "com.android.support.test.espresso:espresso-contrib:3.0.2"
        androidTestImplementation "com.android.support.test.espresso:espresso-idling-resource:3.0.2"
        androidTestImplementation "com.android.support.test.espresso:espresso-intents:3.0.2"
        implementation deps.common_recycleradapter
        implementation deps.support.recyclerview
    }
    

    这样可以通过,但不是最好的解决办法,最好的办法是所有的依赖库support版本都升级到27.1.1,但是有些库是别人提供的并且没有升级的话就很麻烦了。
    ————————————————
    版权声明:本文为CSDN博主「川峰」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/lyabc123456/java/article/details/89875578

    相关文章

      网友评论

          本文标题:单元测试--Espresso测试框架的使用

          本文链接:https://www.haomeiwen.com/subject/aziqmhtx.html