Espresso checking if toasts are displayed (one on

2019-07-30 04:50发布

问题:

I have an problem with checking if toast is displayed using espresso. I'm using class:

       import android.os.IBinder;
    import android.support.test.espresso.Root;
    import android.view.WindowManager;
    import org.hamcrest.Description;
    import org.hamcrest.TypeSafeMatcher;

    public class ToastMatcher extends TypeSafeMatcher<Root> {

    @Override
    public void describeTo(Description description) {
        description.appendText("is toast");
    }

    @Override
    public boolean matchesSafely(Root root) {
        int type = root.getWindowLayoutParams().get().type;
        if ((type == WindowManager.LayoutParams.TYPE_TOAST)) {
            IBinder windowToken = root.getDecorView().getWindowToken();
            IBinder appToken = root.getDecorView().getApplicationWindowToken();
            if (windowToken == appToken) {
                // windowToken == appToken means this window isn't contained by any other windows.
                // if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
                return true;
            }
        }
        return false;
    }

}

and checking Toast by:

onView(withText(R.string.unauthorized)).inRoot(new ToastMatcher())
            .check(matches(isDisplayed()));

Everything works fine until I try to check another toast in the same class for example:

@Test
public void messageOnBack() throws Exception{
pressBack();
onView(withText(R.string.exit_on_back)).inRoot(new ToastMatcher())
            .check(matches(isDisplayed()));

Then first one is passed but second one puts error:

    android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching: with string from resource id: <2131165323>[unauthorized] value: Wrong login or password.

View Hierarchy:
+>LinearLayout{id=-1, visibility=VISIBLE, width=660, height=116, has-focus=false, has-focusable=false, has-window-focus=false, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+->AppCompatTextView{id=16908299, res-name=message, visibility=VISIBLE, width=528, height=58, has-focus=false, has-focusable=false, has-window-focus=false, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=66.0, y=29.0, text=Please click BACK again to exit., input-type=0, ime-target=false, has-links=false}
|
at dalvik.system.VMStack.getThreadStackTrace(Native Method)

What is weird, when I comment out one of the tests second one works just fine without any changes. Espresso seems to get stupid when one toast is displayed on top of another. Any ideas how to solve this?

回答1:

The NoMatchingViewException you are seeing means your ToastMatcher does find a root view with TYPE_TOAST, but cannot find the requested view inside that root (otherwise, you'd get a NoMatchingRootException).

I guess the reason is that Android is not showing toasts on top of each other, but one after the other. Thus, probably the only view it finds in a toast-root is your first toast (your second toast has not yet been shown). Thus, before checking for the second toast you will have to somehow wait until your first toast has disappeared. This isn't trivial, unfortunately (see below), and I believe you cannot get around changing production code.

A possible solution is given in https://stackoverflow.com/a/32023568/1059766. The basic idea is to attach a OnAttachStateChangedListener to your toast's view before showing the toast, and use that listener to track when the view is attached to and detached from the view hierarchy. This can then be used to implement a custom IdlingResource that can wait for a toast to disappear.

The way espresso waits for things is by means of IdlingResources. I currently cannot see how you could create a custom idling resource to wait for a toast without changing production code. Therefore, something along the lines of the aformentioned answer is the best I can think of, even though the required change to production code is not very appealing.

That being said, note that your ToastMatcher solution (that's often suggested on stackoverflow and blogs) is also not a really reliable way to test toasts. It works in most situations, but not always. Consider e.g. the following snippet:

new AsyncTask<Void, Void, Void>() {
    public void doInBackground(...) {
        // start background work for 10s (or just Thread.sleep(10000))
    }
}.execute()
Toast.make(context, R.string.mytoast, Toast.LENGTH_SHORT).show()

As espresso always waits until the UI thread and all async-tasks are idle, it will in the above example wait for (about) 10s until the isDisplayed() check is performed. But at that point the toast will have disappeared and therefore the check fails. I hope this is enough to illustrate the inherent problem with this approach. The following statement from Valera Zakharov in https://groups.google.com/d/msg/android-test-kit-discuss/uaHdXuVm-Bw/cuQASd3PdpgJ seems to confirm that there is no easy solution to test toasts with espresso:

Short answer: Unfortunately, there is no thread-safe way of doing this in Android, so we don't provide this capability in Espresso.
Details: The way Toasts are implemented makes it possible to detect a toast has been displayed. However there is no way to see if a Toast has been requested, thru a call to show()) or to block between the period of time between show() and when the toast has become visible. This is opens up unresolvable timing issues (that you can only address thru sleep & hope) [...].

Zakharov then also suggests to add some hooks to the production code (as far as I understand that). Thus, I guess adding an IdlingResource based on some production code hooks is really the best you can do (this might also make your toast testing more stable in general, as you can then test your toasts as outlined by Zakharov).



回答2:

The problem is that the previously shown toast is showing while you are testing for the second one. You can solve this by making a mToast member variable visible for testing, and use that to cancel any active toast in @After, like this:

When showing toast (the production code for the Activity under test):

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
Toast mToast;

private void showToast(final String text) {
    mToast = Toast.makeText(this, text, Toast.LENGTH_LONG);
    mToast.show();
}

The test code (in the same package as the code under test):

    @After
    public void tearDown() {
        // Remove any toast message that is still shown:
        Toast toast = mActivityRule.getActivity().mToast;
        if (toast != null) {
            toast.cancel();
        }
    }

This will require you to change the production code a tiny bit, but using @VisibleForTesting in the latest version of Android Studio will give error if you use the member variable elsewhere.