Monday, September 7, 2015

Writing ViewHolder Matcher with Espresso for Android.

Recently I had a need to adapt my Espresso tests to operate on RecyclerView after migration from ListViews. The current actions that are available for RecyclerView based on item position working fine but I don't like to be dependent on position since data in my tests is created dynamically.

I've googled the ViewHolder matchers and found only this link without any practical examples - RecyclerViewActions.

Then based on already created Matcher<Object> used in onData(...) I've created Matcher<VH> which was not so difficult.

Let's assume each item in RecyclerView adapter has subject, represented by TextView. The below matcher will search for item in RecylerView with unique subject which I provide into matcher. Feel free to use it:

public static Matcher<RecyclerView.ViewHolder> withItemSubjectInViewHolder(final String itemSubject) {
    Checks.checkNotNull(itemSubject);
    return new BoundedMatcher(RecyclerView.ViewHolder, MyListRecyclerViewItemAdapter.MyViewHolder.class) {
        @Override
        public boolean matchesSafely(MyListRecyclerViewItemAdapter.MyViewHolder holder) {
            boolean isMatches = false;

            if (!(holder.subject == null)) {
                isMatches = ((itemSubject.equals(holder.subject.getText().toString()))
                        && (holder.subject.getVisibility() == View.VISIBLE));
            }
            return isMatches;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("with item subject: " + itemSubject.toString());
        }
    };
}
And the possible usage is:
onView(withId(R.id.adapter_list)).perform(scrollToHolder(withItemSubjectInViewHolder(itemSubject)));
onView(withId(R.id.adapter_list)).perform(actionOnHolderItem(withItemSubjectInViewHolder(itemSubject), click()));

Friday, May 29, 2015

Espresso & UIAutomator - the perfect tandem

Espresso for Android is perfect and fast test automation framework, but it has one important limitation - you are allowed to operate only inside your app under test context.

That means that it is not possible to automate tests for such app features like:

  • application push notifications
  • contact synchronization
  • navigating from another app to your app under test,

since you have to deal with other apps from the mobile device - Notification Bar, Contacts or People app, etc. 

In fact it wasn't possible until the release of UIAutomator 2.0. As stated in Android Developers blog post - "...Most importantly, UI Automator is now based on Android Instrumentation...".  And because of that we can run UIAutomator tests as well as Espresso tests using Instrumentation test runner.

In addition to that we can combine UIAutomator tests together with Espresso tests and this gives us the real power and control over the phone and application under test.

In the below example I'll explain  how this approach was used to automate Contact Synchronization tests for the app I'm working with:

import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiSelector;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.Assert.assertTrue;

@RunWith(AndroidJUnit4.class)
public class TestContactsSync {

    @Rule
    public ActivityTestRule mActivityRule = new ActivityTestRule(LoginActivity.class);

    UiDevice mDevice;
    String PEOPLE_APP = "People";
    String MY_APP = "XING";
    String userContactName = "Android Tester";

    @Before
    public void setUp() throws Exception{
        super.setUp();
        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    }

    @Test
    public void testContactsSync() throws Exception {

        // Espresso code: login the user, navigate to contact sync and enable clicking on toggle
        logInUser();
        onView(withText(R.string.sync_button)).perform(click());
        onView(withId(R.id.contacts_sync_enable_toggle)).perform(click());

        // UIAutomator code: navigate to People app 
        mDevice.pressHome();
        UiObject conutactsApp = mDevice.findObject(new UiSelector().text(PEOPLE_APP));
        conutactsApp.clickAndWaitForNewWindow();

        // UIAutomator code: check that contact is present in it after sync was triggered    
        UiObject contactName = mDevice.findObject(new UiSelector().text(userContactName));
        assertTrue(contactName.exists());

        // UIAutomator code: navigate back to my app under testm
        Device.pressHome();
        UiObject myApp = mDevice.findObject(new UiSelector().text(MY_APP));
        myApp.clickAndWaitForNewWindow();

        // Espresso code: turn off contact sync and logout
        onView(withId(R.id.contacts_sync_enable_toggle)).perform(click());
        onView(withId(R.id.logout)).perform(click());
    }

    @After
    public void tearDown() throws Exception {
        super.tearDown();
    }
}
Isn't it perfect?! And the answer is - almost. UIAutomator requires Android 4.3 (API level 18) or higher. And this can be solved easily by tweaking build.gradle file - adding productFlavors and declare uiautomator dependencies:
productFlavors {
        // The actual application flavor 
        production {
            minSdkVersion 14
        }
        // Test application flavor for uiautomatior tests
        test {
            minSdkVersion 18
        }
    }

dependencies {
    // Instrumentation tests
    androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.1'
    androidTestCompile 'com.android.support.test:rules:0.2'
    // Set this dependency to build and run UI Automator tests
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.0.0'
}
Good luck :)

Thursday, April 9, 2015

Discovering Espresso for Android: wading through the hierarchical thicket

The Android Lollipop update brought a hard nut to crack for the testers who use Espresso, represented by RecyclerView.

So far the app I'm testing contains this element in couple of places. While I was writing tests for them I found out that it was not so easy and obvious. Thanks to Espresso authors, version 2.0 has the basic RecyclerView actions support which is honestly not enough and is not convenient sometimes.

The example of RecyclerView action:
import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
.
.
    @Test
    public void testSomething() {
        onView(withId(R.id.recycler_view_id)).perform(actionOnItemAtPosition(1, click()));
    }
The activity under test has the following structure - it contains the RecyclerView element which is populated with some data. Let's call it the Feed. Each element in Feed is the Feed Post represented by FrameLayout. Of course every FrameLayout has the same layout elements inside as it's neighbors. It contains header with text and footer with like ImageView and TextView used to show number of likes.

What I just described represented by below image:



Now let's try to implement this test case:
  1. click the like ImageView in 3rd RecyclerView item (FameLayout 3), 
  2. check that number of likes changed after the click. 

In my tests I don't want to rely on the Post position in the RecyclerView, because I run multiple tests in parallel and some of them can create new Posts and push down the target one. I'd like to rely on some unique element of the Post instead.

Now I'll focus on the layout. Target ImageView and TextView are marked in red color.


Actually I can't click the like image (ImageView with id=like_image) directly, based on it's id, since this will lead us to multiple matches issue.

But what if we climb in the FrameLayout hierarchy up (from like ImageView) until we found any element with the unique value. In our case the unique is the text in Post header (TextView with id=header_text).

Now we'll find the way how to tie like image with header text. We'll go up in the hierarchy from like image using withParent() method until we reach footer (LinearLayout with id=footer)  which is the sibling of header (LinearLayout with id=header), which contains child with unique text (follow arrows on the image). And the code is:
private void clickOnLikeIconOnPostWithText(String postText){
    onView(allOf(withParent(
            allOf(withParent(
                    allOf(hasSibling(
                            allOf(withId(R.id.header), withChild(withText(postText)))),
                        withId(R.id.footer))),
                withId(R.id.footer_like))),
        withId(R.id.like_image))).perform(click());
}
And the similar code for checking the like text value (TextView with id=like_text):
private void checkLikesText(Matcher<View> likeTextMatcher, String postText){
    onView(allOf(withParent(
            allOf(withParent(
                    allOf(hasSibling(
                            allOf(withId(R.id.header), withChild(withText(postText)))),
                        withId(R.id.footer))),
                withId(R.id.footer_like))),
        withId(R.id.like_text))).check(matches(likeTextMatcher));
}
Finally the usage is:
@Test
public void testPostLike() {
    //like - 1 like count
    clickOnLikeIconOnPostWithText("UNIQUE_TEXT_3");
    checkLikesText(withText("1"),"UNIQUE_TEXT_3");
}
Looks a bit unusual and complicated but it works.

Thursday, February 26, 2015

Discovering Espresso for Android: Espresso 2.0 and 'Class ref in pre-verified class resolved to unexpected implementation' error

This time I would like to share with you solution for 'Class ref in pre-verified class resolved to unexpected implementation' issue I got while I was testing my multi-module project. Espresso tests were failing with below issue when I wanted to operate on RecyclerView from com.android.support:appcompat-v7. In my case RecyclerView dependency was defined not in the core-app module (the core module of the app) but in the other one which is set as a library.
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
at com.my.app.fragments.MyFragment.onCreateView(MyFragment.java:190)
at android.support.v4.app.Fragment.performCreateView(Fragment.java:1786)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:947)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1126)
at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:739)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1489)
at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:548)
at com.my.app.activities.BaseActivity.onStart(BaseActivity.java:226)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1171)
at android.support.test.runner.MonitoringInstrumentation.callActivityOnStart(MonitoringInstrumentation.java:358)
at android.app.Activity.performStart(Activity.java:5143)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2184)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2261)
at android.app.ActivityThread.access$600(ActivityThread.java:141)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1256)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.support.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:461)
at android.support.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:402)
at android.support.test.espresso.base.UiControllerImpl.injectMotionEvent(UiControllerImpl.java:226)
at android.support.test.espresso.action.MotionEvents.sendUp(MotionEvents.java:135)
at android.support.test.espresso.action.MotionEvents.sendUp(MotionEvents.java:118)
at android.support.test.espresso.action.Tap.sendSingleTap(Tap.java:135)
at android.support.test.espresso.action.Tap.access$100(Tap.java:35)
at android.support.test.espresso.action.Tap$1.sendTap(Tap.java:40)
at android.support.test.espresso.action.GeneralClickAction.perform(GeneralClickAction.java:98)
at android.support.test.espresso.ViewInteraction$1.run(ViewInteraction.java:144)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:390)
at java.util.concurrent.FutureTask.run(FutureTask.java:234)
at android.os.Handler.handleCallback(Handler.java:730)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5103)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:737)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
And again solution is similar to one that is posted in my previous post - I have added code below into my core-app module:
configurations {
    androidTestCompile.exclude group: 'com.android.support', module: 'recyclerview-v7'
}
Hope you haven't spent a lot of time looking for the solution :)

Friday, February 13, 2015

Discovering Espresso for Android: Espresso 2.0 and java.lang.NoClassDefFoundError

Currently I'm moving all my tests to Espresso 2.0 and Junit4. And today I was struggling with one nasty issue that you can face with as well and found the solution.

So, the problem was noticed on pre-Lollipop devices/emulators. My app under test is configured to target the current latest API level 21. And after adapting some tests to Espresso 2.0 and JUnit4 I was able to successfully run it on emulator with Lollipop but trying to run it on devices/emulators lower then API 21 was failing with below issue:
java.lang.NoClassDefFoundError: com.my.app.activities.MyActivity
at com.my.app.test.instrumentation.TestMyActivity.(MyActivity.java:52)
at java.lang.reflect.Constructor.constructNative(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
.
.
.
And solution is to add below peace of code into your build.gradle:
configurations {
    androidTestCompile.exclude group: 'com.android.support', module: 'support-v4'
}
:)