Skip to content

Instantly share code, notes, and snippets.

@bharatdodeja
Last active December 17, 2023 12:21
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bharatdodeja/ac001b6a24028bde56943ee40cab7dbd to your computer and use it in GitHub Desktop.
Save bharatdodeja/ac001b6a24028bde56943ee40cab7dbd to your computer and use it in GitHub Desktop.
Android UI testing using Espresso and Robot pattern

Android UI Testing using Espresso, Rotton Pattern and Kotlin DSL

Espresso allow UI tests to have stable interactions with your app, but without discipline these tests can become hard to manage and require frequent updating. Robot pattern allows you to create stable, readable, and maintainable tests with the aid of Kotlin’s language features.

Test Excecution

Let's say there is a login screen and user needs to login access dashboard by entering email and password and clicking on the login button. This is what we want the test to validate. But there’s also the how. Really, what we are looking for is the what.

Traditional Testing (Problem)

A way that you might write the test is something like this:

LoginScreen login = (LoginScreen) obtainScreen();

login.email("john.deo@gmail.com");
login.password.("********");
login.loginButton.click();

Thread.sleep(1000);

assertThat(obtainScreen()).isInstanceOf(SuccessScreen.class);

The problem is that what you’ve essentially done is taken your test and just shoved it into the view and really tightly coupled the two things together.

If our view changes in any significant fashion, we’re going to have to change our test as well. You’re not throwing it out, but you’re refactoring it.

Your test thus ended up being about how the test was being accomplished, not about what was being accomplished.

As a result, whenever you have to change it you have to reinterpret parts of what the test was doing in order to make sure that the behavior is the same on the other side.

Robots: Separating the What From the How (Solution)

What is a robot? Ultimately, this is a pattern and it’s open for interpretation. A robot is just a class that encodes the high level actions that we want to take on a view, or anything else. The how is what we’re going to encode in this robot.

LoginRobot

class LoginRobot {
  LoginRobot userName("john.deo@gmail.com")
  LoginRobot password("***********")
}

class ResultRobot {
  ResultRobot isSuccess()
}

We can enter an email. We don’t know how the email gets entered. The only thing we know is that if we want to enter an email on this screen, it requires some value.

We can enter a password. We don’t know where the password’s going on the screen, we just know that the screen allows us to enter a password field.

Then we can take an action, which is press the Login button. Now, our Login action can actually return a different Result robot that knows how to interact with the screen that’s coming up.

We can verify whether or not that results screen is showing success. Ultimately, if the login failed, some hypothetical assertion happening in here would fail and break your test. There’s all kinds of crap that we can shove in here which is the how. It doesn’t matter what it is.

Now, we have this sweet little API that we can use:

LoginRobot robot = new LoginRobot();
ResultRovot result = robot.
  .userName("john.deo@gmail.com")
  .password("***********")
  .clickLogin();
  
result.isSuccess();

Here, we’ve encoded the what into our test within our builder, as opposed to the how.

The test is declarative, terse, and your robots are a little crazy. The robot is written once, the tests are written many times.

Robots using Kotlin DSL

DSLs are a way of providing an API that is cleaner, more readable and, most importantly, more structured than traditional APIs.

Instead of having to call individual functions in an imperative way, DSLs use nested descriptions, which creates clean structures; we could even call it “grammar”. DSLs define possibilities of combining different constructs with each other and also make copious use of scopes, in which different functionalities can be used.

Our test is okay. It’s nice and declarative and terse. But Kotlin can do us a lot better. We want to try and leverage the language features that Kotlin provides such as higher order functions, infix function, apply etc., while ultimately retaining the type safety aspect of it.

Let’s replace the builder with more advanced primitives of the language. We’re going to switch to a little factory function.

fun loginScreen(func: LoginRobot.() -> Unit): LoginRobot { func () }

class LoginRobot {
  fun userName(email: String) { ... }
  fun password(password: String) { ... }
  infix fun clickLogin(func: ResultRobot.() -> Unit): ResultRobot { 
    ... 
    return ResultRobot().apply { func() }
  }
}

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

By calling the func within the apply block, the function returns a value which is the robot itself, as opposed to void. This will allow us to chain methods nicely.

Now that we have this, what does this change our calling code into?

loginScreen {
  userName("john.deo@gmail.com")
  password("********")
} clickLogin {
  isSuccess()
}

Robot Strategy

The general pattern here starts with an entry point, which is just the first screen you see, and then you have whatever actions you want to take on that view, or assertions. We tend to not do a lot of assertions, we want to push that into more unit tests.

We implicitly assert things by just walking through the app, and ensuring that it does what we expect, and that the things that we want to interact with are on the screen. isSuccess() is like an explicit assertion – you are asserting that something is being displayed on the screen.

Then our clickLogin() method is basically any time you’re transitioning between screens, which are also transitions between robots. Our screens are moving, and we need to move on to another robot and our fancy little infix function here gets that for us.

Descriptive Stack Traces

Another cool thing – we kind of get some nice stack traces out of this that are very descriptive:

Exception in thread "main" java.Lang.AssertionError:
    Expected <Success!> but found <Failure!>
    
    at ResultRobot.isSuccess(ResultRobot.kt:18)
    at LoginTest.singleFundingSourceSuccess.2.invoke(LoginTest.kt:27)
    at LoginRobot.submit(LoginRobot.kt:13)
    at LoginTest.singleFundingSourceSuccess(LoginTest.kt:8)

You don’t have to click on the test, see what line the failure was and kind of figure out what happened. This stack trace essentially becomes very descriptive. We know that we’re in the singleFundingSourceSuccess() test. We know the login robot called clickLogin(). So, it’s pressing the Login button on the login screen.

Then we know the results robot was asserting success. So you get a stack trace, which mimics the steps that were taken in order to get to in this case, the failure. You potentially don’t even have to go look at the test, unless you need the data that was entered. Your stack traces replicate the process through which you got to the failing screen, which is a nice side effect.

The Robot Pattern

We should think of this as a very broad pattern. There’s no library here. This is a pure and simple pattern.

If you actually take the time and think about the architecture of your tests, keeping the same separation of concerns that app architectures give you in your tests, you will end up with higher quality, more maintainable tests that will be correct in the long-term and actually save you from writing code.

Screenshot

Capturing screenshots of instrumentation tests are a great way to generate visual report. It can be part of confluance report of your test suits results.

There are other third party libraries such as Spoon, Falcon, which provides advanced features.

For Screenshot to save screenshots, your app must have the WRITE_EXTERNAL_STORAGE permission.

fun takeScreenshot(parentFolder: String = "Foo", name: String) {
    val screenCapture = Screenshot.capture()
    val processors = setOf(MyScreenCaptureProcessor(parentFolder))
    try {
        screenCapture.apply {
            setName(name)
            process(processors)
        }
    } catch (ex: IOException) {
        Log.e("Screenshots", "Could not take the screenshot", ex)
    }
}

Custom Rule for Test Failure Screenshot

Capturing screenshots when test fails, it is great way of debuging a test failure in a visual way.

To achieve this we need to exptend TestWathcher class and give implementation to failed(). In this method, you will get call before your test fails, so that you can grab a screenshot before activity is finished.

Here is how we can take screenshot of test failure.

class FailureScreenshotRule : TestWatcher() {
  override fun failed(e: Throwable?, description: Description) {
    val parentFolderPath = "failures/${description.className}"
      takeScreenshot(parentFolder = parentFolderPath,
          name = description.methodName)
  }
}

Here is how you will declare custom rule in your Test file.

@get:Rule
val screenShotRule = FailureScreenshotRule()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment