Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement Jake Wharton's robot pattern to Espresso UI testing?

Jake Wharton delivered a fascinating talk where he proposes some smart ways to improve our UI tests by abstracting the detail of how we perform UI out of the tests: https://news.realm.io/news/kau-jake-wharton-testing-robots/

An example he gives is a test that looks as follows, where the PaymentRobot object contains the detail of how the payment amount & recipient are entered into the UI. Putting that one place makes a lot of sense so when the UI inevitably changes (e.g. renaming a field ID, or switching from a TextEdit to a TextInputLayout), it only needs updating in one place not a whole series of tests. It also makes the tests much more terse and readable. He proposes using Kotlin to make them even terser. I do not use Kotlin but still want to benefit from this approach.

@Test public void singleFundingSourceSuccess {
    PaymentRobot payment = new PaymentRobot();
    ResultRobot result = payment
        .amount(42_00)
        .recipient("[email protected]")
        .send();
    result.isSuccess();
}

He provides an outline of how the Robot class may be structured, with an explicit isSuccess() response, returning another Robot which is either the next screen, or the state of the current one:

class PaymentRobot {
    PaymentRobot amount(long amount) { ... }
    PaymentRobot recipient(String recipient) { .. }
    ResultRobot send() { ... }
}

class ResultRobot { 
    ResultRobot isSuccess() { ... }
}

My questions are:

  • How does the Robot interface with the Activity/Fragment, and specifically where is it instantiated? I would expect that happens in the test by the runner, but his examples seem to suggest otherwise. The approach looks like it could be very useful, but I do not see how to implement it in practice, either for a single Activity/Fragment, or for a sequence of them.
  • How can this approach be extended so that the isSuccess() method can handle various scenarios. e.g. if we're testing a Login screen, how can isSuccess() handle various expected results like: authentication success, API network failure, and auth failed (e.g. 403 server response)? Ideally the API would be mocked behind Retrofit, and each result tested with an end to end UI test.

I've not been able to find any examples of implementation beyond Jake's overview talk.

like image 237
Ollie C Avatar asked May 08 '17 12:05

Ollie C


2 Answers

I had totally misunderstood how Espresso works, which led to even more confusion in my mind about how to apply it to the page object pattern. I now see that Espresso does not require any kind of reference to the Activity under test, and just operates within the context of the runner rule. For anyone else struggling, here is a fleshed-out example of applying the robot/page object pattern to a test of the validation on a login screen with a username and password field where we are testing that an error message is shown when either field is empty:

LoginRobot.java (to abstract automation of the login activity)

public class LoginRobot {

    public LoginRobot() {
        onView(withId(R.id.username)).check(matches(isDisplayed()));
    }

    public void enterUsername(String username) {
        onView(withId(R.id.username)).perform(replaceText(username));
    }

    public void enterPassword(String password) {
        onView(withId(R.id.password)).perform(replaceText(password));
    }

    public void clickLogin() {
        onView(withId(R.id.login_button)).perform(click());
    }

}

(Note that the constructor is testing to make sure the current screen is the screen we expect)

LoginValidationTests.java:

@LargeTest
@RunWith(AndroidJUnit4.class)
public class LoginValidationTests {

    @Rule
    public ActivityTestRule<LoginActivity> mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);

    @Test
    public void loginPasswordValidationTest() {
        LoginRobot loginPage = new LoginRobot();
        loginPage.enterPassword("");
        loginPage.enterUsername("123");
        loginPage.clickLogin();
        onView(withText(R.string.login_bad_password))
 .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
    }

    @Test
    public void loginUsernameValidationTest() {
        LoginRobot loginPage = new LoginRobot();
        loginPage.enterUsername("");
        loginPage.enterPassword("123");
        loginPage.clickLogin();
        onView(withText(R.string.login_bad_username)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
    }

}

Abstracting the mechanisms to automate the UI like this avoids a mass of duplication across tests, and also means changes are less likely to need to be reflected across many tests. e.g. if a layout ID changes, it only needs updating in the robot class, not every test that refers to that field. The tests are also significantly shorter and more readable.

The robot methods, e.g. the login button method, can return the next robot in the chain (ie that operating on the activity after the login screen). e.g. LoginRobot.clickLogin() returns a HomeRobot (for the app's main home screen).

I've put the assertions in the tests, but if assertions are reused in many tests it might make sense to abstract some into the robot.

In some cases it might make sense to use a view model object to hold a set of fake data that is reused across tests. e.g. if testing a registration screen with many fields that is operated on by many tests it might make sense to build a factory to create a RegistrationViewModel that contains the first name, last name, email address etc, and refer to that in tests rather than duplicating that code.

like image 68
Ollie C Avatar answered Nov 02 '22 05:11

Ollie C


How does the Robot interface with the Activity/Fragment, and specifically where is it instantiated? I would expect that happens in the test by the runner, but his examples seem to suggest otherwise. The approach looks like it could be very useful, but I do not see how to implement it in practice, either for a single Activity/Fragment, or for a sequence of them.

The Robot is supposed to be a utility class used for the purpose of the test. It's not supposed to be a production code included as a part of your Fragment/Activity or whatever you wanna use. Jake's analogy is pretty much perfect. The Robot acts like a person interacting with the screen of the app. So the exposed api of a Robot should be screen specific, regardless of what your implementation is underneath. It can be across multiple activities, fragments, dialogs, etc. or it can reflect an interaction with just a single component. It really depends on your application and test cases you have.


How can this approach be extended so that the isSuccess() method can handle various scenarios. e.g. if we're testing a Login screen, how can isSuccess() handle various expected results like: authentication success, API network failure, and auth failed (e.g. 403 server response)? Ideally the API would be mocked behind Retrofit, and each result tested with an end to end UI test.

The API of your Robot really should specify the what in your tests. Not the how. So from the perspective of using a Robot it wouldn't care if you got the API network failure or the auth failure. This is how. "How did you end up with a failure". A human QA tester (=== Robot) wouldn't look at http stream to notice the difference between api failure or http timeout. He/she would only see that your screen said Failure. The Robot would only care if that was a Success or Failure.

The other thing you might want to test here is whether your app showed a message notifying user of a connection error (regardless of the exact cause).

class ResultRobot { 
    ResultRobot isSuccess() { ... }
    ResultRobot isFailure() { ... }
    ResultRobot signalsConnectionError() { ... }
}

result.isFailure().signalsConnectionError();
like image 37
Bartek Lipinski Avatar answered Nov 02 '22 05:11

Bartek Lipinski