Android automated testing technology - the use of Espresso

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.gradleAdd 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.gradlethe file android.defaultConfigadd

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 XXXcalled 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):
insert image description here
Common api instances pdf

use

common control

Example: MainActivityContains a Buttonand a TextView. After clicking the button, TextViewthe content of will change to "Changed successfully" .

Use Espressoto 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. EspressoProvides 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 TextViewthat that entry is included. Spinnerwill 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:
insert image description here

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 RecyclerViewthe operations, let's take a look at how to customize Matcherand 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

RecyclerViewObjects work AdapterViewdifferently than objects, so you cannot onData()interact with them using the methods.
To interact Espressowith with RecyclerView, you can use espresso-contribthe package, which has RecyclerViewActionsa 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 RecyclerVieware:

  • 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 , RecycleViewand 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 RecycleViewthe 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: MatcherDataActionCalled 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-Intentsa test, it needs to be set up IntentsTestRule. This is ActivityTestRulean extension of the class that allows you to easily use the API in functional interface tests Espresso-Intents. IntentsTestRuleWill be @Testinitialized before each annotated test run Espresso-Intentsand 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-Intentsthe 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-IntentsA 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. EspressoThese 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 SimpleIdlingResourcea 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: EspressoProvides a set of advanced synchronization functions. However, this feature of the framework only applies to MessageQueueoperations that publish messages on the , such as View subclasses that draw content to the screen.

other

EspressoThere 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

reference

Guess you like

Origin blog.csdn.net/lichukuan/article/details/126861863