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的某些状态
//根据id匹配
onView(withId(R.id.my_view))
//根据文本匹配
onView(withText("Hello World!"))
//点击
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的本文内容是否匹配“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是否弹出,可以这么写:
@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的方法跟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());
}
修改代码将上面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
获取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在创建之前会涉及到一些第三方库的依赖,这时直接跑测试方法会报错。
@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指定目标页面需要的权限,在测试方法运行前会自动授权。
直接看图:
点击某个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的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
匹配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)))));
}
}
虽然单元测试不建议处理异步的操作,但是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在Run菜单中提供了一个Record Espresso Test
功能,选择之后可以将用户的操作记录下来并转成测试代码。
选择之后,会弹出一个记录面板:
接下来你在手机上针对应用的每一步操作会被记录下来,最后点击Ok就会自动生成刚才操作的case代码了。试了一下这个功能的想法还是很好的,可是操作起来太卡了,不太实用。
为方便测试Espresso专门为WebView提供了一个支持库,具体参考官方介绍:Espresso Web
单元测试很少用,直接看官方的介绍: Multiprocess Espresso
直接看官方的介绍:Accessibility checking
更多Espresso的Demo可以参考官方的介绍:Additional Resources for Espresso 基本上demo都是在Github上面的, 本文中找不到的例子可以到里面找找看。
最后再来一张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,但是有些库是别人提供的并且没有升级的话就很麻烦了
所有代码已上传至Gitee。希望大家多多点赞支持!有什么问题及建议也可以提Issues,让我们将他慢慢的完善起来。