Loading...

"The solution often turns out more beautiful than the puzzle."

Arushi Pant

Always eager to learn. Determined to make the best of the time I've got on Earth.

Espresso: Run tests based on the device

Posted on September 12, 2018

Espresso testing

This learning is a part of my Android Developer Nanodegree[1] offered by Udacity & Google. I was writing Instrumentation tests with Espresso for my Project #4 Baking App, and faced a particular problem. The problem I faced is quite a common issue if you have different layout/implementation for different devices, and you want to write Espresso tests.

Let me explain my problem statement a bit further. 

 

Problem Statement: I have a different layout for phone (Coordinator layout), and a different one for tablet (Master-detail layout). Now, I wish to run some specific tests only for phones (to check the Coordinator layout), and some tests only for tablets (to check Master-detail layout).

How do I run an Espresso test only for a tablet or a phone?

 

I also give you a sample scenario: I want all the tests to be in CI, and I don't want manual intervention in deciding what device is currently being tested. There are various tests that depend on the device configuration. Some tests will run only on tablets, and some only on phones.

Whether a test is run or not should be decided in run-time, according to the device running the code.

 

Working towards the Solution:

A very nice article on this has been written by Aitor Viana - Espresso: Do not assume, just annotate

I would suggest going through it before you proceed with this one. The method he stated almost worked for me, expect some small details we will discuss later.

 

So, as Aitor suggests, we start by creating two annotations @PhoneTest  and @TabletTest.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface PhoneTest {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TabletTest {
}

 

And then, we annotate our tests with these annotations we created.

@RunWith(AndroidJUnit4.class)
public class SomeActivityTestClass {

    @PhoneTest 
    @Test
    public void phoneSpecificTest() {
        // ... code ...
    }

    @TabletTest
    @Test
    public void tabletSpecificTest() {
        // ... code ...
    }
}

 

Now, we have to make those annotations work. There needs to be a method that will look at the annotation and decide whether the test will be run or not.

As this method will be used across tests, it makes sense to create a separate class BaseTest with all the common methods. This BaseTest class will be extended by all our other test classes.

The BaseTest class will use the TestName rule to check the annotations of the current test method that is about to be executed, and according to the device, decide whether to run the test.

 

public class BaseTest {
    public Activity mActivity;

    @Rule
    public TestName testName = new TestName();

    @Before
    public void setUp() throws Exception {
        /* IMP: requires setting mActivity in any class that extends this */
        assertDeviceOrSkip();
    }

    private void assertDeviceOrSkip() {
        try {
            Method m = getClass().getMethod(testName.getMethodName());
            if (m.isAnnotationPresent(TabletTest.class)) {
                assumeTrue(isTablet());
            } else if (m.isAnnotationPresent(PhoneTest.class)) {
                assumeTrue(isPhone());
            }
        } catch (NoSuchMethodException e) {
            // Do nothing
        }
    }

    private boolean isPhone() {
        return !isTablet();
    }

    private boolean isTablet() {
        return mActivity.getResources().getBoolean(R.bool.isTablet);
    }
}

 

The major logic is in the method assertDeviceOrSkip() that decides whether the test is run. 

 

We have to check the configurations for the device running the tests, and confirm whether the device isPhone() or isTablet(). This can be done with the help of resources and qualifiers.

Set a boolean value (isTablet = true) in res/values-sw600dp/booleans.xml, and in res/values-xlarge/booleans.xml 

<resources>
    <bool name="isTablet">true</bool>
</resources>

 

 Set the boolean value to false in the "standard" file res/values/boolean.xml

<resources>
    <bool name="isTablet">false</bool>
</resources>

 

When I was following Aitor's article, I got stuck with the issue that there was no Context available in the BaseTest. I was not able to call getResources().

I solved this by adding an Activity variable mActivity to the BaseTest class. This variable will be set in the class that extends BaseTest.

Now, we can call mActivity.getResources() in the BaseTest for the resources.

 

We go ahead, and extend our "SomeActivityTestClass" from the BaseTest class.

Notice how we assign the variable mActivity in the setUp() before calling the super.setUp() method.

@RunWith(AndroidJUnit4.class)
public class SomeActivityTestClass extends BaseTest {

    @Before
    public void setUp() throws Exception {
        mActivity = mActivityTestRule.getActivity();
        super.setUp();
    }

    @PhoneTest 
    @Test
    public void phoneSpecificTest() {
        // ... code ...
    }

    @TabletTest
    @Test
    public void tabletSpecificTest() {
        // ... code ...
    }
}

 

All the coding is now done for our solution.

I just love how this works. The tests are all in one codebase, and I do not have to manually decide which ones to run for checking the code in the device.

 

[1] Referral link - Will give you a Rs. 1000 cashback on your first Nanodegree enrollment


Liked the post?

Show your appreciation, and share with others.