Android自动化测试技术——Espresso的使用

配置

修改设置

先启用开发者选项,再在开发者选项下,停用以下三项设置:

  • 窗口动画缩放
  • 过渡动画缩放
  • Animator 时长缩放

添加依赖

app/build.gradle文件中添加依赖

androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

androidTestImplementation 'androidx.test:runner:1.2.0'

androidTestImplementation 'androidx.test:rules:1.2.0'

app/build.gradle文件中的android.defaultConfig中添加

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

注意:上面的依赖只能实现基本功能,如果你想使用所有的功能,则按下面的配置:

所有依赖

    androidTestImplementation 'androidx.test.ext:junit:1.1.1'

androidTestImplementation 'androidx.test.ext:truth:1.2.0'

androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'

androidTestImplementation 'androidx.test:runner:1.2.0'

androidTestImplementation 'androidx.test:rules:1.2.0'

androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'

implementation 'androidx.recyclerview:recyclerview:1.1.0'

implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'

下面调用的方法如onView()等都是静态方法,可以通过import static XXX来直接调用,所有需要导入的静态方法如下:

import static androidx.test.espresso.Espresso.*;

import static androidx.test.espresso.action.ViewActions.*;

import static androidx.test.espresso.assertion.ViewAssertions.*;

import static androidx.test.espresso.intent.Intents.intended;

import static androidx.test.espresso.intent.Intents.intending;

import static androidx.test.espresso.intent.matcher.ComponentNameMatchers.*;

import static androidx.test.espresso.intent.matcher.IntentMatchers.*;

import static androidx.test.espresso.matcher.ViewMatchers.*;

import static androidx.test.ext.truth.content.IntentSubject.assertThat;

Api组件

常用Api组件包括:

  • Espresso - 用于与视图交互(通过 onView() 和 onData())的入口点。此外,还公开不一定与任何视图相关联的 API,如 pressBack()。
  • ViewMatchers - 实现 Matcher<? super View> 接口的对象的集合。您可以将其中一个或多个对象传递给 onView() 方法,以在当前视图层次结构中找到某个视图。
  • ViewActions - 可传递给 ViewInteraction.perform() 方法的 ViewAction 对象(如 click())的集合。
  • ViewAssertions - 可传递给 ViewInteraction.check() 方法的 ViewAssertion 对象的集合。在大多数情况下,您将使用 matches 断言,它使用视图匹配器断言当前选定视图的状态。

大多数可用的 Matcher、ViewAction 和 ViewAssertion 实例如下图(来源官方文档):

常用的api实例pdf

使用

普通控件

示例:MainActivity 包含一个 Button 和一个 TextView。点击该按钮后,TextView 的内容会变为 "改变成功"。

使用 Espresso 进行测试方法如下:

@RunWith(AndroidJUnit4.class)

@LargeTest

publicclassChangeTextTest{

@Rule

public ActivityTestRule<MainActivity> activityRule =

new ActivityTestRule<>(MainActivity.class);

@Test

publicvoidtest_change_text(){

onView(withId(R.id.change))

.perform(click());

onView(withId(R.id.content))

.check(matches(withText("改变成功")));

}

}

onView()方法用来获取匹配的当前视图,注意匹配的视图只能有一个,否则会报错。

withId()方法用来搜索匹配的视图,类似的还有withText()withHint()等。

perform()方法用来执行某种操作,例如点击click() 、长按longClick() 、双击doubleClick()

check()用来将断言应用于当前选定的视图

matches()最常用的断言,它断言当前选定视图的状态。上面的示例就是断言id为content的View它是否和text为"改变成功"的View匹配

AdapterView相关控件

与普通控件不同,AdapterView(常用的是ListView)只能将一部分子视图加载到当前视图层次结构中。简单的 onView() 搜索将找不到当前未加载的视图。Espresso 提供一个单独的 onData() 入口点,该入口点能够先加载相关适配器项目,并在对其或其任何子级执行操作之前使其处于聚焦状态。

示例:打开Spinner,选择一个特定的条目,然后验证 TextView 是否包含该条目。Spinner 会创建一个包含其内容的 ListView,因此需要onData()

@RunWith(AndroidJUnit4.class)

@LargeTest

publicclassSpinnerTest{

@Rule

public ActivityTestRule<MainActivity> activityRule =

new ActivityTestRule<>(MainActivity.class);

@Test

publicvoidtest_spinner(){

String content = "学校";

//点击Spnner,显示项目

onView(withId(R.id.change)).perform(click());

//点击指定的内容

onData(allOf(is(instanceOf(String.class)), is(content))).perform(click());

//判断TextView是否包含指定内容

onView(withId(R.id.content))

.check(matches(withText(containsString(content))));

}

}

下图为AdapterView的继承关系图:

警告:如果 AdapterView 的自定义实现违反继承约定,那么在使用 onData() 方法(尤其是 getItem() API)时可能会出现问题。在这种情况下,最好的做法是重构应用代码。如果您无法执行此操作,则可以实现匹配的自定义 AdapterViewProtocol。

自定义Matcher和ViewAction

在介绍RecyclerView的操作之前,我们先要看看如何自定义MatcherViewAction

自定义Matcher

Matcher<T>是一个用来匹配视图的接口,常用的是它的两个实现类BoundedMatcher<T, S extends T>

TypeSafeMatcher<T>

BoundedMatcher<T, S extends T>:一些匹配的语法糖,可以让你创建一个给定的类型,而匹配的特定亚型的只有过程项匹配。

类型参数:<T> - 匹配器的期望类型。<S> - T的亚型

TypeSafeMatcher<T>:内部实现了空检查,检查的类型,然后进行转换

示例:输入EditText值,如果值以000开头,则让内容为 "成功" 的TextView可见,否则让内容为 失败 的TextView可见.

@RunWith(AndroidJUnit4.class)

@LargeTest

publicclassEditTextTest{

@Rule

public ActivityTestRule<MainActivity> activityRule =

new ActivityTestRule<>(MainActivity.class);

@Test

publicvoidrightInput(){

onView(withId(R.id.editText))

.check(matches(EditMatcher.isRight()))

.perform(typeText("000123"), ViewActions.closeSoftKeyboard());

onView(withId(R.id.button)).perform(click());

onView(withId(R.id.textView_success)).check(matches(isDisplayed()));

onView(withId(R.id.textView_fail)).check(matches(not(isDisplayed())));

}

@Test

publicvoiderrorInput(){

onView(withId(R.id.editText))

.check(matches(EditMatcher.isRight()))

.perform(typeText("003"), ViewActions.closeSoftKeyboard());

onView(withId(R.id.button)).perform(click());

onView(withId(R.id.textView_success)).check(matches(not(isDisplayed())));

onView(withId(R.id.textView_fail)).check(matches(isDisplayed()));

}

staticclassEditMatcher{

static Matcher<View> isRight(){

//自定义Matcher

returnnew BoundedMatcher<View, EditText>(EditText.class) {

@Override

publicvoiddescribeTo(Description description){

description.appendText("EditText不满足要求");

}

@Override

protectedbooleanmatchesSafely(EditText item){

//在输入EditText之前,先判EditText是否可见以及hint是否为指定值

if (item.getVisibility() == View.VISIBLE &&

item.getText().toString().isEmpty())

returntrue;

else

returnfalse;

}

};

}

}

}

自定义ViewAction

这个不太熟悉,这里就介绍一下实现ViewAction接口,要实现的方法的作用

/**

*符合某种限制的视图

*/

public Matcher<View> getConstraints();

/**

*返回视图操作的描述。 *说明不应该过长,应该很好地适应于一句话

*/

public String getDescription();

/**

* 执行给定的视图这个动作。

*PARAMS:uiController - 控制器使用与UI交互。

*view - 在采取行动的view。 不能为null

*/

publicvoidperform(UiController uiController, View view);

}

RecyclerView

RecyclerView 对象的工作方式与 AdapterView 对象不同,因此不能使用 onData() 方法与其交互。

要使用 EspressoRecyclerView 交互,您可以使用 espresso-contrib 软件包,该软件包具有 RecyclerViewActions的集合,定义了用于滚动到相应位置或对项目执行操作的方法。

添加依赖

androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'

操作RecyclerView的方法有:

  • scrollTo() - 滚动到匹配的视图。
  • scrollToHolder() - 滚动到匹配的视图持有者。
  • scrollToPosition() - 滚动到特定位置。
  • actionOnHolderItem() - 对匹配的视图持有者执行视图操作。
  • actionOnItem() - 对匹配的视图执行视图操作。
  • actionOnItemAtPosition() - 在特定位置对视图执行视图操作。

示例:选中删除功能:点击 编辑 ,TextView内容转为 删除 ,同时RecycleView的条目出现选中框,勾选要删除的项,点击 删除 ,删除指定项,RecycleView的条目的选中框消失。

@RunWith(AndroidJUnit4.class)

@LargeTest

publicclassRecyclerViewTest{

@Rule

public ActivityTestRule<RecyclerActivity> activityRule =

new ActivityTestRule<>(RecyclerActivity.class);

staticclassClickCheckBoxActionimplementsViewAction{

@Override

public Matcher<View> getConstraints(){

return any(View.class);

}

@Override

public String getDescription(){

returnnull;

}

@Override

publicvoidperform(UiController uiController, View view){

CheckBox box = view.findViewById(R.id.checkbox);

box.performClick();//点击

}

}

staticclassMatcherDataActionimplementsViewAction{

private String require;

publicMatcherDataAction(String require){

this.require = require;

}

@Override

public Matcher<View> getConstraints(){

return any(View.class);

}

@Override

public String getDescription(){

returnnull;

}

@Override

publicvoidperform(UiController uiController, View view){

TextView text = view.findViewById(R.id.text);

assertThat("数据值不匹配",require,equalTo(text.getText().toString()));

}

}

publicvoiddelete_require_data(){

//获取RecyclerView中显示的所有数据

List<String> l = new ArrayList<>(activityRule.getActivity().getData());

//点击 编辑 ,判断text是否变成 删除

onView(withId(R.id.edit))

.perform(click())

.check(matches(withText("删除")));

//用来记录要删除的项,

Random random = new Random();

int time = random.nextInt(COUNT);

List<String> data = new ArrayList<>(COUNT);

for (int i = 0; i < COUNT; i++) {

data.add("");

}

for (int i = 0; i < time; i++) {

//随机生成要删除的位置

int position = random.nextInt(COUNT);

//由于再次点击会取消,这里用来记录最后确定要删除的项

if (data.get(position).equals(""))

data.set(position,"测试数据"+position);

else data.set(position,"");

//调用RecyclerViewActions.actionOnItemAtPosition()方法,滑到指定位置

//在执行指定操作

onView(withId(R.id.recycler)).

perform(RecyclerViewActions.actionOnItemAtPosition(position,new ClickCheckBoxAction()));

}

//点击 删除 ,判断text是否变成 编辑

onView(withId(R.id.edit))

.perform(click(),doubleClick())

.check(matches(withText("编辑")));

//删除无用的项

data.removeIf(s -> s.equals(""));

//获取最后保存的项

l.removeAll(data);

//依次判断保留的项是否还存在

for (int i = 0; i < l.size(); i++) {

final String require = l.get(i);

onView(withId(R.id.recycler))

.perform(RecyclerViewActions.

actionOnItemAtPosition(i,new MatcherDataAction(require)));

}

}

}

注意:在MatcherDataAction中调用了assertThat(),这种方式是不建议的。这里是我没有找到更好的方式来实现这个测试。

Intent

Espresso-Intents 是 Espresso 的扩展,支持对被测应用发出的 Intent 进行验证和打桩。

添加依赖:

androidTestImplementation 'androidx.test.ext:truth:1.2.0'

androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'

在编写 Espresso-Intents 测试之前,要先设置 IntentsTestRule。这是 ActivityTestRule 类的扩展,可让您在功能界面测试中轻松使用 Espresso-Intents的API。IntentsTestRule 会在带有 @Test 注解的每个测试运行前初始化Espresso-Intents,并在每个测试运行后释放 Espresso-Intents

 @Rule

public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(

MainActivity.class);

验证 Intent

示例:在EditText中,输入电话号码,点击拨打按键,拨打电话。

@RunWith(AndroidJUnit4.class)

@LargeTest

publicclassIntentTest{

//设置拨打电话的权限的环境

@Rule

public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant("android.permission.CALL_PHONE");

@Rule

public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(

MainActivity.class);

@Test

publicvoidtest_start_other_app_intent(){

String phoneNumber = "123456";

//输入电话号码

onView(withId(R.id.phone))

.perform(typeText(phoneNumber), ViewActions.closeSoftKeyboard());

//点击拨打

onView(withId(R.id.button))

.perform(click());

//验证Intent是否正确

intended(allOf(

hasAction(Intent.ACTION_CALL),

hasData(Uri.parse("tel:"+phoneNumber))));

}

}

intended():是Espresso-Intents 提供的用来验证Intent的方法

除此之外,还可以通过断言的方式来验证Intent

Intent receivedIntent = Iterables.getOnlyElement(Intents.getIntents());

assertThat(receivedIntent)

.extras()

.string("phone")

.isEqualTo(phoneNumber);

插桩

上述方式可以解决一般的Intent验证的操作,但是当我们需要调用startActivityForResult()方法去启动照相机获取照片时,如果使用一般的方式,我们就需要手动去点击拍照,这样就不算自动化测试了。

Espresso-Intents 提供了intending()方法来解决这个问题,它可以为使用 startActivityForResult() 启动的 Activity 提供桩响应。简单来说就是,它不会去启动照相机,而是返回你自己定义的Intent。

@RunWith(AndroidJUnit4.class)

@LargeTest

publicclassTakePictureTest{

publicstatic BoundedMatcher<View, ImageView> hasDrawable(){

returnnew BoundedMatcher<View, ImageView>(ImageView.class) {

@Override

publicvoiddescribeTo(Description description){

description.appendText("has drawable");

}

@Override

publicbooleanmatchesSafely(ImageView imageView){

return imageView.getDrawable() != null;

}

};

}

@Rule

public IntentsTestRule<MainActivity> mIntentsRule = new IntentsTestRule<>(

MainActivity.class);

@Rule

public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA);

@Before

publicvoidstubCameraIntent(){

Instrumentation.ActivityResult result = createImageCaptureActivityResultStub();

intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);

}

@Test

publicvoidtakePhoto_drawableIsApplied(){

//先检查ImageView中是否已经设置了图片

onView(withId(R.id.image)).check(matches(not(hasDrawable())));

// 点击拍照

onView(withId(R.id.button)).perform(click());

// 判断ImageView中是否已经设置了图片

onView(withId(R.id.image)).check(matches(hasDrawable()));

}

private Instrumentation.ActivityResult createImageCaptureActivityResultStub(){

//自己定义Intent

Bundle bundle = new Bundle();

bundle.putParcelable("data", BitmapFactory.decodeResource(

mIntentsRule.getActivity().getResources(), R.drawable.ic_launcher_round));

Intent resultData = new Intent();

resultData.putExtras(bundle);

returnnew Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);

}

}

空闲资源

空闲资源表示结果会影响界面测试中后续操作的异步操作。通过向 Espresso 注册空闲资源,可以在测试应用时更可靠地验证这些异步操作。

添加依赖

implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'

下面以Google的官方示例来介绍,如何使用:

第一步:创建SimpleIdlingResource类,用来实现IdlingResource

publicclassSimpleIdlingResourceimplementsIdlingResource{

@Nullable

privatevolatile ResourceCallback mCallback;

private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);

@Override

public String getName(){

returnthis.getClass().getName();

}

/**

*false 表示这里有正在进行的任务,而true表示异步任务完成

*/

@Override

publicbooleanisIdleNow(){

return mIsIdleNow.get();

}

@Override

publicvoidregisterIdleTransitionCallback(ResourceCallback callback){

mCallback = callback;

}

publicvoidsetIdleState(boolean isIdleNow){

mIsIdleNow.set(isIdleNow);

if (isIdleNow && mCallback != null) {

//调用这个方法后,Espresso不会再检查isIdleNow()的状态,直接判断异步任务完成

mCallback.onTransitionToIdle();

}

}

}

第二步:创建执行异步任务的类MessageDelayer

classMessageDelayer{

privatestaticfinalint DELAY_MILLIS = 3000;

interfaceDelayerCallback{

voidonDone(String text);

}

staticvoidprocessMessage(final String message, final DelayerCallback callback,

@Nullable final SimpleIdlingResource idlingResource){

if (idlingResource != null) {

idlingResource.setIdleState(false);

}

Handler handler = new Handler();

new Thread(()->{

try {

Thread.sleep(DELAY_MILLIS);

} catch (InterruptedException e) {

e.printStackTrace();

}

handler.post(new Runnable() {

@Override

publicvoidrun(){

if (callback != null) {

callback.onDone(message);

if (idlingResource != null) {

idlingResource.setIdleState(true);

}

}

}

});

}).start();

}

}

第三步:在MainActivity中通过点击按钮开启任务

publicclassMainActivityextendsAppCompatActivityimplementsView.OnClickListener,

MessageDelayer.DelayerCallback{

private TextView mTextView;

private EditText mEditText;

@Nullable

private SimpleIdlingResource mIdlingResource;

@Override

protectedvoidonCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

findViewById(R.id.changeTextBt).setOnClickListener(this);

mTextView = findViewById(R.id.textToBeChanged);

mEditText = findViewById(R.id.editTextUserInput);

}

@Override

publicvoidonClick(View view){

final String text = mEditText.getText().toString();

if (view.getId() == R.id.changeTextBt) {

mTextView.setText("正在等待");

MessageDelayer.processMessage(text, this, mIdlingResource);

}

}

@Override

publicvoidonDone(String text){

mTextView.setText(text);

}

/**

* 仅测试能调用,创建并返回新的SimpleIdlingResource

*/

@VisibleForTesting

@NonNull

public IdlingResource getIdlingResource(){

if (mIdlingResource == null) {

mIdlingResource = new SimpleIdlingResource();

}

return mIdlingResource;

}

}

第四步:创建测试用例

@RunWith(AndroidJUnit4.class)

@LargeTest

publicclassChangeTextBehaviorTest{

privatestaticfinal String STRING_TO_BE_TYPED = "Espresso";

private IdlingResource mIdlingResource;

/**

*注册IdlingResource实例

*/

@Before

publicvoidregisterIdlingResource(){

ActivityScenario activityScenario = ActivityScenario.launch(MainActivity.class);

activityScenario.onActivity((ActivityScenario.ActivityAction<MainActivity>) activity -> {

mIdlingResource = activity.getIdlingResource();

IdlingRegistry.getInstance().register(mIdlingResource);

});

}

@Test

publicvoidchangeText_sameActivity(){

onView(withId(R.id.editTextUserInput))

.perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());

onView(withId(R.id.changeTextBt)).perform(click());

//只需要注册IdlingResource实例,Espresso就会自动在这里等待,直到异步任务完成

//在执行下面的代码

onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));

}

//取消注册

@After

publicvoidunregisterIdlingResource(){

if (mIdlingResource != null) {

IdlingRegistry.getInstance().unregister(mIdlingResource);

}

}

}

不足:Espresso提供了一套先进的同步功能。不过,该框架的这一特性仅适用于在 MessageQueue 上发布消息的操作,如在屏幕上绘制内容的 View 子类。

其他

Espresso还有在多进程、WebView、无障碍功能检查、多窗口等内容,这些我不太熟悉,建议自己看

安卓官方文档或者下面的官方示例。

官方示例

  • IntentsBasicSample:intended() 和 intending() 的基本用法。

  • IdlingResourceSample:与后台作业同步。

  • BasicSample:基本的 Espresso 示例。

  • CustomMatcherSample:展示如何扩展 Espresso 以与 EditText 对象的 hint 属性匹配。

  • DataAdapterSample:展示 Espresso 中适用于列表和 AdapterView 对象的 onData() 入口点。

  • IntentsAdvancedSample:模拟用户使用相机获取位图。

  • MultiWindowSample:展示如何将 Espresso 指向不同的窗口。

  • RecyclerViewSample:Espresso 的 RecyclerView 操作。

  • WebBasicSample:使用 Espresso-Web 与 WebView 对象交互。

参考

  • Android测试(一):在Android中测试App
  • Android单元测试-常见的方案比较
  • 测试分为什么,白盒,黑盒,单元,集成测试?
  • 安卓官方文档Espresso部分

以上是 Android自动化测试技术——Espresso的使用 的全部内容, 来源链接: utcz.com/a/21161.html

回到顶部