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

2020-04-09  本文已影响0人  lisx_

原文链接:川峰-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 由以下三个基础部分组成:

获取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接口的三个方法含义:

@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

上一篇下一篇

猜你喜欢

热点阅读