configuration
modify settings
First enable the developer options, and then under the developer options, disable the following three settings:
- Window animation scaling
- transition animation scaling
- Animator duration scaling
add dependencies
app/build.gradle
Add dependencies to the file
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
In app/build.gradle
the file android.defaultConfig
add
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Note: The above dependencies can only achieve basic functions. If you want to use all functions, please configure as follows:
all dependencies
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'
The methods called below onView()
are static methods, and can be import static XXX
called directly by . All static methods that need to be imported are as follows:
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 component
Commonly used API components include:
- Espresso - Entry point for interacting with views (via onView() and onData()). Additionally, APIs such as pressBack() that are not necessarily associated with any view are exposed.
- ViewMatchers - A collection of objects implementing the Matcher<? super View> interface. You can pass one or more of these objects to the onView() method to find a view in the current view hierarchy.
- ViewActions - A collection of ViewAction objects (such as click() ) that can be passed to the ViewInteraction.perform() method.
- ViewAssertions - A collection of ViewAssertion objects that can be passed to the ViewInteraction.check() method. In most cases, you'll use matches assertions, which use view matchers to assert the state of the currently selected view.
Most of the available Matcher, ViewAction and ViewAssertion instances are as follows (source official documents):
Common api instances pdf
use
common control
Example: MainActivity
Contains a Button
and a TextView
. After clicking the button, TextView
the content of will change to "Changed successfully" .
Use Espresso
to test as follows:
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void test_change_text(){
onView(withId(R.id.change))
.perform(click());
onView(withId(R.id.content))
.check(matches(withText("改变成功")));
}
}
onView()
The method is used to obtain the matching current view. Note that there can only be one matching view, otherwise an error will be reported.
withId()
The method is used to search for matching views, similarly withText()
, withHint()
etc.
perform()
Method is used to perform some action, such as click click()
, long press longClick()
, double clickdoubleClick()
check()
Used to apply assertions to the currently selected view
matches()
The most commonly used assertion, it asserts the state of the currently selected view. The above example is to assert whether the View whose id is content matches the View whose text is "change successful"
AdapterView related controls
Unlike normal controls, AdapterView
(commonly ListView
) only a subset of subviews can be loaded into the current view hierarchy. A simple onView()
search will not find views that are not currently loaded. Espresso
Provides a single onData()
entry point that first loads the relevant adapter item and brings it into focus before performing operations on it or any of its children.
Example: open Spinner
, select a specific entry, then verify TextView
that that entry is included. Spinner
will create a containing its content ListView
, so the need foronData()
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SpinnerTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void test_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))));
}
}
The following figure shows the inheritance relationship diagram of AdapterView:
Warning: If the custom implementation of AdapterView violates the inheritance contract, then there may be problems when using the onData() method (especially the getItem() API). In this case, the best course of action is to refactor the application code. If you can't do this, you can implement a matching custom AdapterViewProtocol.
Custom Matcher and ViewAction
Before introducing RecyclerView
the operations, let's take a look at how to customize Matcher
and ViewAction
.
customizeMatcher
Matcher<T>
is an interface used to match the view, commonly used are its two implementation classes BoundedMatcher<T, S extends T>
andTypeSafeMatcher<T>
BoundedMatcher<T, S extends T>
: Some matching syntactic sugar that allows you to create a match of a given type while matching only process items of a particular subtype.
Type parameters:<T> - 匹配器的期望类型。<S> - T的亚型
TypeSafeMatcher<T>
: Internally implements null checks, checks the type, and then converts
Example: Enter the EditText value, if the value starts with 000
, make the TextView with the content "success" visible, otherwise make the TextView with the content " failure " visible.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class EditTextTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void rightInput() {
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
public void errorInput() {
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()));
}
static class EditMatcher{
static Matcher<View> isRight(){
//自定义Matcher
return new BoundedMatcher<View, EditText>(EditText.class) {
@Override
public void describeTo(Description description) {
description.appendText("EditText不满足要求");
}
@Override
protected boolean matchesSafely(EditText item) {
//在输入EditText之前,先判EditText是否可见以及hint是否为指定值
if (item.getVisibility() == View.VISIBLE &&
item.getText().toString().isEmpty())
return true;
else
return false;
}
};
}
}
}
customizeViewAction
This is not very familiar, here is an introduction to the implementation of the ViewAction interface, the function of the method to be implemented
/**
*符合某种限制的视图
*/
public Matcher<View> getConstraints();
/**
*返回视图操作的描述。 *说明不应该过长,应该很好地适应于一句话
*/
public String getDescription();
/**
* 执行给定的视图这个动作。
*PARAMS:uiController - 控制器使用与UI交互。
*view - 在采取行动的view。 不能为null
*/
public void perform(UiController uiController, View view);
}
RecyclerView
RecyclerView
Objects work AdapterView
differently than objects, so you cannot onData()
interact with them using the methods.
To interact Espresso
with with RecyclerView
, you can use espresso-contrib
the package, which has RecyclerViewActions
a collection that defines methods for scrolling to positions or performing actions on items.
add dependencies
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
The methods of operation RecyclerView
are:
- scrollTo() - Scrolls to the matching view.
- scrollToHolder() - Scrolls to the matching view holder.
- scrollToPosition() - scroll to a specific position.
- actionOnHolderItem() - Performs a view action on a matching view holder.
- actionOnItem() - Performs a view action on the matching view.
- actionOnItemAtPosition() - Performs a view action on the view at a specific position.
Example: Select the delete function: click Edit , the TextView content will be changed to delete , RecycleView
and a check box will appear for the item at the same time, check the item to be deleted, click Delete , delete the specified item, and RecycleView
the check box of the item will disappear.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class RecyclerViewTest {
@Rule
public ActivityTestRule<RecyclerActivity> activityRule =
new ActivityTestRule<>(RecyclerActivity.class);
static class ClickCheckBoxAction implements ViewAction{
@Override
public Matcher<View> getConstraints() {
return any(View.class);
}
@Override
public String getDescription() {
return null;
}
@Override
public void perform(UiController uiController, View view) {
CheckBox box = view.findViewById(R.id.checkbox);
box.performClick();//点击
}
}
static class MatcherDataAction implements ViewAction{
private String require;
public MatcherDataAction(String require) {
this.require = require;
}
@Override
public Matcher<View> getConstraints() {
return any(View.class);
}
@Override
public String getDescription() {
return null;
}
@Override
public void perform(UiController uiController, View view) {
TextView text = view.findViewById(R.id.text);
assertThat("数据值不匹配",require,equalTo(text.getText().toString()));
}
}
public void delete_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)));
}
}
}
Note: MatcherDataAction
Called in assertThat()
, this method is not recommended. Here's a test I haven't found a better way to implement.
Intent
Espresso-Intents is an extension of Espresso, which supports verification and piling of Intents sent by the application under test.
Add dependencies:
androidTestImplementation 'androidx.test.ext:truth:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
Before writing Espresso-Intents
a test, it needs to be set up IntentsTestRule
. This is ActivityTestRule
an extension of the class that allows you to easily use the API in functional interface tests Espresso-Intents
. IntentsTestRule
Will be @Test
initialized before each annotated test run Espresso-Intents
and deallocated after each test run Espresso-Intents
.
@Rule
public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
MainActivity.class);
Verify Intent
Example: In EditText, enter a phone number, click the dial button, and make a call.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntentTest {
//设置拨打电话的权限的环境
@Rule
public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant("android.permission.CALL_PHONE");
@Rule
public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
MainActivity.class);
@Test
public void test_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()
: is Espresso-Intents
the method provided to verify Intent
In addition, Intent can also be verified by assertion
Intent receivedIntent = Iterables.getOnlyElement(Intents.getIntents());
assertThat(receivedIntent)
.extras()
.string("phone")
.isEqualTo(phoneNumber);
stake
The above method can solve the general Intent verification operation, but when we need to call startActivityForResult()
the method to start the camera to obtain photos, if we use the general method, we need to manually click to take a photo, which is not considered an automated test.
Espresso-Intents
A method is provided intending()
to solve this problem by providing a stub response for an Activity started with startActivityForResult(). Simply put, it will not start the camera, but return your own defined Intent.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class TakePictureTest {
public static BoundedMatcher<View, ImageView> hasDrawable() {
return new BoundedMatcher<View, ImageView>(ImageView.class) {
@Override
public void describeTo(Description description) {
description.appendText("has drawable");
}
@Override
public boolean matchesSafely(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
public void stubCameraIntent() {
Instrumentation.ActivityResult result = createImageCaptureActivityResultStub();
intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);
}
@Test
public void takePhoto_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);
return new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
}
}
idle resources
A free resource indicates an asynchronous operation whose results affect subsequent operations in the UI test. Espresso
These asynchronous operations can be verified more reliably when testing your application by registering idle resources with .
add dependencies
implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'
Let's use Google's official example to introduce how to use:
The first step: create SimpleIdlingResource
a class to implementIdlingResource
public class SimpleIdlingResource implements IdlingResource {
@Nullable
private volatile ResourceCallback mCallback;
private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);
@Override
public String getName() {
return this.getClass().getName();
}
/**
*false 表示这里有正在进行的任务,而true表示异步任务完成
*/
@Override
public boolean isIdleNow() {
return mIsIdleNow.get();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mCallback = callback;
}
public void setIdleState(boolean isIdleNow) {
mIsIdleNow.set(isIdleNow);
if (isIdleNow && mCallback != null) {
//调用这个方法后,Espresso不会再检查isIdleNow()的状态,直接判断异步任务完成
mCallback.onTransitionToIdle();
}
}
}
Step 2: Create a class that performs asynchronous tasksMessageDelayer
class MessageDelayer {
private static final int DELAY_MILLIS = 3000;
interface DelayerCallback {
void onDone(String text);
}
static void processMessage(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
public void run() {
if (callback != null) {
callback.onDone(message);
if (idlingResource != null) {
idlingResource.setIdleState(true);
}
}
}
});
}).start();
}
}
Step 3: Start the task by clicking the button in MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener,
MessageDelayer.DelayerCallback {
private TextView mTextView;
private EditText mEditText;
@Nullable
private SimpleIdlingResource mIdlingResource;
@Override
protected void onCreate(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
public void onClick(View view) {
final String text = mEditText.getText().toString();
if (view.getId() == R.id.changeTextBt) {
mTextView.setText("正在等待");
MessageDelayer.processMessage(text, this, mIdlingResource);
}
}
@Override
public void onDone(String text) {
mTextView.setText(text);
}
/**
* 仅测试能调用,创建并返回新的SimpleIdlingResource
*/
@VisibleForTesting
@NonNull
public IdlingResource getIdlingResource() {
if (mIdlingResource == null) {
mIdlingResource = new SimpleIdlingResource();
}
return mIdlingResource;
}
}
Step 4: Create test cases
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {
private static final String STRING_TO_BE_TYPED = "Espresso";
private IdlingResource mIdlingResource;
/**
*注册IdlingResource实例
*/
@Before
public void registerIdlingResource() {
ActivityScenario activityScenario = ActivityScenario.launch(MainActivity.class);
activityScenario.onActivity((ActivityScenario.ActivityAction<MainActivity>) activity -> {
mIdlingResource = activity.getIdlingResource();
IdlingRegistry.getInstance().register(mIdlingResource);
});
}
@Test
public void changeText_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
public void unregisterIdlingResource() {
if (mIdlingResource != null) {
IdlingRegistry.getInstance().unregister(mIdlingResource);
}
}
}
Insufficient:
Espresso
Provides a set of advanced synchronization functions. However, this feature of the framework only applies toMessageQueue
operations that publish messages on the , such as View subclasses that draw content to the screen.
other
Espresso
There are also multi-process, WebView, accessibility check, multi-window, etc. I am not familiar with these, so I suggest you read the official
Android documentation or the official examples below.
official example
- IntentsBasicSample : Basic usage of intended() and intending().
- IdlingResourceSample : Synchronized with background jobs.
- BasicSample : A basic Espresso sample.
- CustomMatcherSample : Shows how to extend Espresso to match the hint property of an EditText object.
- DataAdapterSample : Demonstrates the onData() entry point in Espresso for List and AdapterView objects.
- IntentsAdvancedSample : Simulates the user using the camera to acquire a bitmap.
- MultiWindowSample : Shows how to point Espresso to different windows.
- RecyclerViewSample : Espresso's RecyclerView action.
- WebBasicSample : Use Espresso-Web to interact with a WebView object.