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
的操作之前,我们先要看看如何自定义Matcher
和ViewAction
。
自定义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()
方法与其交互。
要使用 Espresso
与 RecyclerView
交互,您可以使用 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
。
@Rulepublic 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