configuration
Change the setting
First enable the Developer option, then under the Developer option, disable the following three Settings:
- Window animation zoom
- Transition animation zoom
- Animator zoom in time
Add the dependent
Add dependencies to app/build.gradle file
androidTestImplementation 'androidx. Test. Espresso: espresso - core: 3.2.0'
androidTestImplementation 'androidx. Test: runner: 1.2.0'
androidTestImplementation 'androidx. Test: rules: 1.2.0'
Copy the code
Add it in android. DefaultConfig in app/build.gradle
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Copy the code
Note: The above dependencies only implement basic functionality. If you want to use all functionality, follow the following configuration:
All depend on
androidTestImplementation 'androidx. Test. Ext: junit: 1.1.1'
androidTestImplementation 'androidx. Test. Ext: way: 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 - the intents: 3.2.0'
implementation 'androidx. Recyclerview: recyclerview: 1.1.0'
implementation 'androidx. Test. Espresso: espresso - idling - resource: 3.2.0'
Copy the code
The following methods, such as onView(), are static and can be called directly by importing 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;
Copy the code
Api components
Common Api components include:
- Espresso – The entry point for interacting with the view (via onView() and onData()). In addition, apis that are not necessarily associated with any view are exposed, such as pressBack().
- ViewMatchers – implement Matcher
a collection of objects for the 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 – Set of ViewAssertions objects that can be passed to the viewinteraction.check () method. In most cases, you’ll use matches assertion, which uses the view matcher to assert the state of the currently selected view.
Most of the available Matcher, ViewAction, and ViewAssertion instances are shown below (source: official documentation) :
Common API examples PDF
use
Common control
Example :MainActivity contains a Button and a TextView. When you click the button, the content of the TextView will change to “Changed successfully.”
The test method using Espresso is as follows:
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void test_change_text(a){
onView(withId(R.id.change))
.perform(click());
onView(withId(R.id.content))
.check(matches(withText("Change for success"))); }}Copy the code
The onView() method is used to get the current view of the match. Note that only one view can be matched, otherwise an error will be reported.
The withId() method is used to search for matching views, as are withText(), withHint(), and so on.
The perform() method is used to perform operations such as click(), longClick(), or doubleClick doubleClick()
Check () is used to apply assertions to the currently selected view
Matches (), the most commonly used assertion, asserts the state of the currently selected view. The example above is to assert whether a View with id content matches a View with text “changed successfully”
AdapterView related controls
Unlike normal controls, AdapterView(most commonly ListView) can only load a subset of its child views into the current view hierarchy. A simple onView() search will not find the currently loaded view. Espresso provides a single onData() entry point that can first load the associated adapter project and put it in focus before performing operations on it or any of its children.
Example: Open a Spinner, select a specific entry, and verify that TextView contains it. The Spinner will create a ListView containing its contents, so onData() is required
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SpinnerTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void test_spinner(a){
String content = "School";
// Click Spnner to display the project
onView(withId(R.id.change)).perform(click());
// Click the specified content
onData(allOf(is(instanceOf(String.class)), is(content))).perform(click());
// Determine whether the TextView contains the specified contentonView(withId(R.id.content)) .check(matches(withText(containsString(content)))); }}Copy the code
The following is an AdapterView inheritance diagram:
Warning: If the custom implementation of AdapterView violates inheritance conventions, problems may arise when using the onData() method, especially the getItem() API. In such cases, it is best to refactor the application code. If you are unable to do this, you can implement a matching custom AdapterViewProtocol.
Customize Matcher and ViewAction
Before we look at RecyclerView operations, let’s look at how to customize Matcher and ViewAction.
The customMatcher
Matcher
is an interface for matching views. The two implementation classes BoundedMatcher
and TypeSafeMatcher
are commonly used.
BoundedMatcher
: Some matching syntactic sugar that allows you to create a given type that matches only procedure items of a particular subtype. Type parameter:
– The expected type of the matcher.
-t subtype
TypeSafeMatcher
: Internally implements null checking, checks for types, and then converts
Example: Enter the EditText value to make textViews with “successful” content visible if the value starts with 000, and failed textViews otherwise.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class EditTextTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void rightInput(a) {
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(a) {
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(a){
// Custom Matcher
return new BoundedMatcher<View, EditText>(EditText.class) {
@Override
public void describeTo(Description description) {
description.appendText("EditText does not satisfy this requirement.");
}
@Override
protected boolean matchesSafely(EditText item) {
Before entering EditText, check whether the EditText is visible and whether the hint is the specified value
if (item.getVisibility() == View.VISIBLE &&
item.getText().toString().isEmpty())
return true;
else
return false; }}; }}}Copy the code
The customViewAction
I’m not familiar with this, but I’m going to introduce you to the methods that implement the ViewAction interface
/** * the view conforms to some restriction */
public Matcher<View> getConstraints(a);
/** * Returns a description of the view operation. * Instructions should not be too long and should fit nicely into a single sentence */
public String getDescription(a);
/** * Performs the action for the given view. *PARAMS: uiController - The controller uses to interact with the UI. * View - View in action. The value cannot be null */
public void perform(UiController uiController, View view);
}
Copy the code
RecyclerView
RecyclerView objects work differently from AdapterView objects, so you can’t interact with them using the onData() method. To interact with RecyclerView using Espresso, you can use the Espresso-Contrib package, which has a collection of RecyclerViewActions that define methods for scrolling to the appropriate location or performing operations on the project.
Add the dependent
androidTestImplementation 'androidx. Test. Espresso: espresso - contrib: 3.2.0'
Copy the code
The method of RecyclerView operation is:
- ScrollTo () – scrollTo the matching view.
- ScrollToHolder () – Scroll to the matched view holder.
- ScrollToPosition () – Scroll to a specific position.
- ActionOnHolderItem () – Performs view action on the matched view holder.
- ActionOnItem () – Performs view action on matched views.
- ActionOnItemAtPosition () – Performs view action on the view at a specific location.
Example: Select delete function: click edit, TextView content to delete, and RecycleView items appear selected box, check the items to delete, click delete, delete the specified item,RecycleView items of the selected box disappeared.
@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(a) {
return any(View.class);
}
@Override
public String getDescription(a) {
return null;
}
@Override
public void perform(UiController uiController, View view) {
CheckBox box = view.findViewById(R.id.checkbox);
box.performClick();/ / click}}static class MatcherDataAction implements ViewAction{
private String require;
public MatcherDataAction(String require) {
this.require = require;
}
@Override
public Matcher<View> getConstraints(a) {
return any(View.class);
}
@Override
public String getDescription(a) {
return null;
}
@Override
public void perform(UiController uiController, View view) {
TextView text = view.findViewById(R.id.text);
assertThat("Data value mismatch",require,equalTo(text.getText().toString())); }}public void delete_require_data(a){
// Get all the data displayed in RecyclerView
List<String> l = new ArrayList<>(activityRule.getActivity().getData());
// Click Edit to see if text becomes delete
onView(withId(R.id.edit))
.perform(click())
.check(matches(withText("Delete")));
// To record the item to be deleted,
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++) {
// Randomly generate the location to delete
int position = random.nextInt(COUNT);
// Since clicking again cancels, this is used to record the items that are finally deleted
if (data.get(position).equals(""))
data.set(position,"Test data"+position);
else data.set(position,"");
/ / call RecyclerViewActions. ActionOnItemAtPosition () method, slip to the specified location
// The specified operation is performed
onView(withId(R.id.recycler)).
perform(RecyclerViewActions.actionOnItemAtPosition(position,new ClickCheckBoxAction()));
}
// Click Delete to determine if text becomes edit
onView(withId(R.id.edit))
.perform(click(),doubleClick())
.check(matches(withText("Edit")));
// Delete useless items
data.removeIf(s -> s.equals(""));
// Get the last saved item
l.removeAll(data);
// Check whether the reserved items still exist
for (int i = 0; i < l.size(); i++) {
final String require = l.get(i);
onView(withId(R.id.recycler))
.perform(RecyclerViewActions.
actionOnItemAtPosition(i,newMatcherDataAction(require))); }}}Copy the code
Note that assertThat() is called in the MatcherDataAction, which is not recommended. Here’s where I didn’t find a better way to implement this test.
Intent
Espresso-intents is an extension of Espresso that supports validation and staking of Intents issued by the application under test.
Add dependencies:
androidTestImplementation 'androidx. Test. Ext: way: 1.2.0'
androidTestImplementation 'androidx. Test. Espresso: espresso - the intents: 3.2.0'
Copy the code
Before writing an espresso-Intents test, set up IntentsTestRule. This is an extension of the ActivityTestRule class that makes it easy to use the ESpress-Intents API for functional interface testing. IntentsTestRule initializes espress-Intents before each Test run with the @test annotation and releases espress-Intents after each Test run.
@Rule
public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
MainActivity.class);
Copy the code
Verify the Intent
Example: In EditText, enter a phone number and press the Dial button to make a phone call.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntentTest {
// Set the environment for calling permissions
@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(a){
String phoneNumber = "123456";
// Enter the phone number
onView(withId(R.id.phone))
.perform(typeText(phoneNumber), ViewActions.closeSoftKeyboard());
// Click dial
onView(withId(R.id.button))
.perform(click());
// Verify the Intent is correct
intended(allOf(
hasAction(Intent.ACTION_CALL),
hasData(Uri.parse("tel:"+phoneNumber)))); }}Copy the code
Intended (): A method provided by Espresso-Intents to validate the Intent
In addition, you can validate an Intent with assertions
Intent receivedIntent = Iterables.getOnlyElement(Intents.getIntents());
assertThat(receivedIntent)
.extras()
.string("phone")
.isEqualTo(phoneNumber);
Copy the code
Insert the pile
The above method can handle normal Intent validation, but when we call the startActivityForResult() method to start the camera to get the photo, we need to manually click The photo, which is not an automated test.
Espresso-Intents provides the regulation () method to solve this problem, which can provide the stem response for the Activity started using startActivityForResult(). Simply put, it doesn’t launch the camera, but returns the Intent you define yourself.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class TakePictureTest {
public static BoundedMatcher<View, ImageView> hasDrawable(a) {
return new BoundedMatcher<View, ImageView>(ImageView.class) {
@Override
public void describeTo(Description description) {
description.appendText("has drawable");
}
@Override
public boolean matchesSafely(ImageView imageView) {
returnimageView.getDrawable() ! =null; }}; }@Rule
public IntentsTestRule<MainActivity> mIntentsRule = new IntentsTestRule<>(
MainActivity.class);
@Rule
public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA);
@Before
public void stubCameraIntent(a) {
Instrumentation.ActivityResult result = createImageCaptureActivityResultStub();
intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);
}
@Test
public void takePhoto_drawableIsApplied(a) {
// Check if the ImageView already has an image
onView(withId(R.id.image)).check(matches(not(hasDrawable())));
// Click Take photo
onView(withId(R.id.button)).perform(click());
// Determine if the ImageView already has an image
onView(withId(R.id.image)).check(matches(hasDrawable()));
}
private Instrumentation.ActivityResult createImageCaptureActivityResultStub(a) {
// Define your own 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 newInstrumentation.ActivityResult(Activity.RESULT_OK, resultData); }}Copy the code
Idle resources
An idle resource represents an asynchronous operation whose results affect subsequent operations in the interface test. By registering idle resources with Espresso, you can verify these asynchronous operations more reliably when testing your application.
Add the dependent
implementation 'androidx. Test. Espresso: espresso - idling - resource: 3.2.0'
Copy the code
Here is an official example from Google to introduce how to use it:
Step 1: Create a SimpleIdlingResource class that implements IdlingResource
public class SimpleIdlingResource implements IdlingResource {
@Nullable
private volatile ResourceCallback mCallback;
private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);
@Override
public String getName(a) {
return this.getClass().getName();
}
/** *false indicates that there is an ongoing task, while true indicates that the asynchronous task has completed */
@Override
public boolean isIdleNow(a) {
return mIsIdleNow.get();
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mCallback = callback;
}
public void setIdleState(boolean isIdleNow) {
mIsIdleNow.set(isIdleNow);
if(isIdleNow && mCallback ! =null) {
// After calling this method, Espresso does not check isIdleNow() status and determines that the asynchronous task is completemCallback.onTransitionToIdle(); }}}Copy the code
Step 2: Create a MessageDelayer class that performs asynchronous tasks
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(a) {
if(callback ! =null) {
callback.onDone(message);
if(idlingResource ! =null) {
idlingResource.setIdleState(true); }}}}); }).start(); }}Copy the code
Step 3: Start the task in MainActivity by clicking the button
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("Waiting");
MessageDelayer.processMessage(text, this, mIdlingResource); }}@Override
public void onDone(String text) {
mTextView.setText(text);
}
/** * Only tests can call, create and return a new SimpleIdlingResource */
@VisibleForTesting
@NonNull
public IdlingResource getIdlingResource(a) {
if (mIdlingResource == null) {
mIdlingResource = new SimpleIdlingResource();
}
returnmIdlingResource; }}Copy the code
Step 4: Create test cases
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {
private static final String STRING_TO_BE_TYPED = "Espresso";
private IdlingResource mIdlingResource;
/** * Register IdlingResource instance */
@Before
public void registerIdlingResource(a) {
ActivityScenario activityScenario = ActivityScenario.launch(MainActivity.class);
activityScenario.onActivity((ActivityScenario.ActivityAction<MainActivity>) activity -> {
mIdlingResource = activity.getIdlingResource();
IdlingRegistry.getInstance().register(mIdlingResource);
});
}
@Test
public void changeText_sameActivity(a) {
onView(withId(R.id.editTextUserInput))
.perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
onView(withId(R.id.changeTextBt)).perform(click());
// Just register the IdlingResource instance and Espresso will automatically wait here until the asynchronous task completes
// Execute the following code
onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));
}
// Cancel registration
@After
public void unregisterIdlingResource(a) {
if(mIdlingResource ! =null) { IdlingRegistry.getInstance().unregister(mIdlingResource); }}}Copy the code
Downside :Espresso offers an advanced set of synchronization features. However, this feature of the framework only applies to operations that publish messages on MessageQueue, such as View subclasses that draw content on the screen.
other
Espresso also has in the multi-process, WebView, barrier-free function check, multi-window and other content, these I am not familiar with, I suggest you see the official Android documentation or the following official example.
The official sample
- IntentsBasicSample: intending() and intending() basic usage.
- IdlingResourceSample: Synchronizes with background jobs.
- BasicSample: A basic Espresso sample.
- CustomMatcherSample: shows how to extend Espresso to match the Hint property of the EditText object.
- DataAdapterSample: Shows the onData() entry point for lists and AdapterView objects in Espresso.
- IntentsAdvancedSample: Simulates the user using the camera to obtain a bitmap.
- MultiWindowSample: Shows how to point Espresso to different Windows.
- RecyclerViewSample: Espresso’s RecyclerView operation.
- WebBasicSample: Interact with a WebView object using espresso-Web.
reference
- Android Testing (1) : Test the App on Android
- Android Unit Testing – Common scenario comparisons
- What are the tests, white box, black box, unit, integration?
- Android official documentation Espresso section