Edition 51

Calling Methods Inside Your App From Appium

Appium is traditionally considered a "black box" testing tool, meaning it has no access to your application's internal methods or state. We use Appium correctly by thinking like a user would (interacting with the surface of the app), not thinking like an app developer would (calling internal code directly).

Black box testing has its limitations, primarily in requiring the automation to go through user steps many times, even when it would be more convenient to skip to a certain known state. This is one reason that some modern testing technologies, like Espresso, allow for a white box testing approach, where internal app methods are accessible from the automation context.

Thanks to Appium's Espresso driver, Appium can now take advantage of this approach. (If you recall, Espresso is also what enabled us to make our elements flash on screen). To make this more general white box strategy work, you need two things:

  1. Knowledge of a particular public method located on your Android application, activity, or UI element. This is the method that your test script will ultimately trigger in the course of your automation. You can either code this method up yourself or ping your Android app developer to add one that meets your specifications.
  2. The new mobile: backdoor method available on the Appium Espresso driver. This is the actual method you will call in your test code, which will tell the Espresso driver what to run inside your app. (It's called "backdoor" because Appium is getting inside of your app through the "back door" of Espresso, not the "front door" of the UI that a user would use. This approach was first publicly suggested by Rajdeep Varma in his AppiumConf 2018 talk, and Rajdeep was the one who contributed the code to make it a reality in Appium today. Thanks!)

To illustrate how this works, I've added a method to the application class of The App, in its MainApplication.java:

public void raiseToast(String message) {
    Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}

This method simply takes an arbitrary string and uses it to make an Android Toast message appear on the screen. The result of calling this method looks like this:

Hello, Toast

With a normal Appium test, there's no way I would be able to trigger this toast to appear, unless the developer had hooked it up to a text field and a button. But with mobile: backdoor, I can simply designate the name of the method I want to call in my app, the types and values of its parameters, and off we go! So here's how I would do this in Java:

ImmutableMap<String, Object> scriptArgs = ImmutableMap.of(
    "target", "application",
    "methods", Arrays.asList(ImmutableMap.of(
        "name", "raiseToast",
        "args", Arrays.asList(ImmutableMap.of(
            "value", "Hello from the test script!",
            "type", "String"
        ))
    ))
);

driver.executeScript("mobile: backdoor", scriptArgs);

It's a little verbose, so let's look at the parameter I'm passing to mobile: backdoor as a JSON object instead:

{
    "target": "application",
    "methods": [{
        "name": "raiseToast",
        "args": [{
            "value": "Hello from the test script!",
            "type": "String"
        }]
    }]
}

I specify two main bits of information: the target of my backdoor (which type of thing am I calling the method on), and the methods I want to call on it. In this case I have implemented my method on the application class, so I specify application as the target. Other possible values are activity (for methods implemented on the current activity), or element (for methods implemented on a specific UI element--in this case, an elementId parameter is also required).

The most important information resides in methods. In our case I'm just calling one method, though we could call multiple. For each method, we have to specify its name (raiseToast--it must exactly match the name in my Android code), and the potentially multiple arguments we want to pass in to call the method with. For each of these arguments in turn we must specify both its type and value (the type is necessary because Java!).

Once we've got all this put together, we simply bundle it up and pass it as the parameter to executeScript("mobile: backdoor"). Using the ImmutableMap.of construction as I've shown above is the most concise way I've found so far to do this in Java.

That's all there is to it! This method in conjunction with the Espresso driver frees us from the shackles of the UI, and enables us to target specific methods inside our app. The possibilities here are endless, so please write to us and let us know what cool things you find to do with the feature. But remember, with great power comes great responsibility! It would probably be very easy to use this feature to crash your app during testing, too.

Here's a full example that shows the toast message being raised on our test app:

import com.google.common.collect.ImmutableMap;
import io.appium.java_client.AppiumDriver;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.remote.DesiredCapabilities;

public class Edition051_Android_Backdoor {

    private String APP = "https://github.com/cloudgrey-io/the-app/releases/download/v1.8.1/TheApp-v1.8.1.apk";

    private AppiumDriver driver;

    @Before
    public void setUp() throws IOException {
        DesiredCapabilities caps = new DesiredCapabilities();

        caps.setCapability("platformName", "Android");
        caps.setCapability("deviceName", "Android Emulator");
        caps.setCapability("automationName", "Espresso");
        caps.setCapability("app", APP);
        driver = new AppiumDriver(new URL("http://localhost:4723/wd/hub"), caps);
    }

    @After
    public void tearDown() {
        try {
            driver.quit();
        } catch (Exception ign) {}
    }

    @Test
    public void testBackdoor() {
        ImmutableMap<String, Object> scriptArgs = ImmutableMap.of(
            "target", "application",
            "methods", Arrays.asList(ImmutableMap.of(
                "name", "raiseToast",
                "args", Arrays.asList(ImmutableMap.of(
                    "value", "Hello from the test script!",
                    "type", "String"
                ))
            ))
        );

        driver.executeScript("mobile: backdoor", scriptArgs);
        try { Thread.sleep(2000); } catch (Exception ign) {} // pause to allow visual verification
    }

}

(As always, the full code sample is also up on GitHub)