Edition 118

Testing Real-Time User Interaction Using Multiple Simultaneous Appium Sessions

Two Appium Devices

Many of the apps that we use, and need to test, involve the real-time interaction of multiple users, who are sitting in front of their own devices or browsers. The "standard" Appium test involves one driver object, representing one Appium session, automating a single device. How can we extend our usage of Appium so that we can coordinate multiple devices, the way multiple users would spontaneously coordinate their own behavior?

Examples of multi-user interaction

First, let's take a look at some common use cases for a multi-user or multi-device test:

  1. A chat application involving one or more users interacting in real time
  2. A ridesharing app, involving two parties---the rider, who requests a ride using an app, and the driver, who can accept and pick up a rider using the same app. Rider and driver stay in constant interaction through the updating of a map that shows their positions.
  3. A food delivery app, involving three parties---the person requesting the food, the restaurant preparing the food, and the delivery person who picks up the food from the restaurant and delivers it to the requester.

Of course, lots of other apps involve user interaction that isn't necessarily real-time. Any social app, for example, will have the ability for users to like or comment on other users' posts. In a normal case, it would be possible to conceive of this as happening asynchronously, not in real time. But many of these same apps also want to be able to test that the UI is updated in real time, when another user interacts with some content. So, for example, when I'm using Twitter, I get an in-app notification (rather than a system notification), if someone likes or retweets one of my posts. Testing this particular feature requires the same kind of multi-user interaction we're talking about here.

Background for a multi-device flow

The basic point I want to convey in this guide is that Appium needs no special help to accommodate a multi-device, multi-user, real-time test scenario. This is true because of the following facts about Appium (which you might already be aware of):

  1. Appium sessions are represented by objects in your test code. There's nothing to stop you from having multiple such objects.
  2. Appium servers can handle multiple simultaneous sessions (with appropriate config to make sure sessions don't step on each other's toes).

These two facts make it technically trivial (if not always intuitive from a test development perspective) to implement real-time multi-device flows. Now, there is a little caveat on number 2 above---because Appium sessions utilize certain system resources (primarily TCP ports) to communicate with specific devices, if you want to run multiple Appium sessions at once on a single machine, you'll need to include some capabilities that direct Appium to use specific ports for each session. This info has already been covered in the Appium Pro article on running multiple Appium tests in parallel. There is no difference from Appium's perspective between the same test being run in parallel on different devices, or one test incorporating multiple devices to test a user interaction flow.

Our multi-user AUT

To showcase multi-device testing with Appium, I developed a simple chat application, called Appium Pro Chat. It's a non-persisent multi-channel chat room, with zero memory, data storage, or (I hesitate to add) security. Feel free to go try it out in your browser! You can open up two browsers or tabs, pick a username, and join the same channel to see it in action. The basic test that I will implement is this:

  1. User 1 pick a username and join a channel
  2. User 2 pick a username and join the same channel as User 1
  3. User 1 and User 2 both type things into the chat
  4. Assert that the final chat log contains both of their messages in the correct order

It's a simple test, but one that has to involve the coordination of multiple Appium devices and multiple Appium sessions.

Multi-device setup

For this example, I am going to use an iOS simulator for User 1, and an Android device (Galaxy S7) for User 2. The first thing I need to do is make sure that I can start an Appium session on each of these, so I've created a helper method for each that just returns an Appropriate driver instance.

private IOSDriver<WebElement> getSafari() throws MalformedURLException {
    DesiredCapabilities capabilities = new DesiredCapabilities();
    capabilities.setCapability("platformName", "iOS");
    capabilities.setCapability("platformVersion", "13.3");
    capabilities.setCapability("deviceName", "iPhone 11");
    capabilities.setCapability("browserName", "Safari");
    capabilities.setCapability("automationName", "XCUITest");
    return new IOSDriver<>(new URL("http://localhost:4723/wd/hub"), capabilities);
}

private AndroidDriver<WebElement> getChrome() throws MalformedURLException {
    DesiredCapabilities capabilities = new DesiredCapabilities();
    capabilities.setCapability("platformName", "Android");
    capabilities.setCapability("deviceName", "Android Emulator");
    capabilities.setCapability("browserName", "Chrome");
    capabilities.setCapability("automationName", "UiAutomator2");
    return new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), capabilities);
}

Now, in my test setUp method, I make sure to instantiate both drivers, which are stored as fields in my class, for example (omitting the other features of this class):

public class Edition118_Multi_Device {
    private IOSDriver<WebElement> safari;
    private AndroidDriver<WebElement> chrome;

    @Before
    public void setUp() throws MalformedURLException {
        safari = getSafari();
        chrome = getChrome();
    }

    @After
    public void tearDown() {
        if (safari != null) {
            safari.quit();
        }
        if (chrome != null) {
            chrome.quit();
        }
    }
}

You can see that there's nothing special to get multiple sessions going at the same time---I just first create the session on Safari on iOS, then the one on Chrome on Android. Setup and teardown now handle two drivers, instead of one. In this example, I am instantiating these sessions serially; if I wanted to optimize further, I could potentially instantiate them at the same time in separate threads, but it adds unnecessary complexity at this point.

Coordinating multiple drivers within a single test

Now, we need to use both of these drivers (the safari and chrome objects) in such a way that they interact in real time with one another, in the course of a single test. Before I show the test method itself, I'll explain the plan: each of the two users will trade lines from the powerful Langston Hughes poem Harlem. To encapsulate the data which will be used to support the test, we'll add it all as a set of fields at the top of the class:

private static final String CHAT_URL = "https://chat.appiumpro.com";
private static final String CHANNEL = "Harlem";
private static final String USER1_NAME = "Langston";
private static final String USER2_NAME = "Hughes";
private static final ImmutableList<String> USER1_CHATS = ImmutableList.of(
        "What happens to a dream deferred?",
        "Or fester like a sore---and then run?",
        "Or crust and sugar over---like a syrupy sweet?",
        "Or does it explode?");
private static final ImmutableList<String> USER2_CHATS = ImmutableList.of(
        "Does it dry up like a raisin in the sun?",
        "Does it stink like rotten meat?",
        "Maybe it just sags like a heavy load.",
        "........yes");

(Note that I have, perhaps ill-advisedly, but I believe in line with the spirit of the poem, added a final line of my own, partly to make User 2's list of messages equal the length of User 1's). With the data set up in ImmutableLists, we're in a position to iterate over the chats and implement the user interaction in a very concise way.

Next, I wanted to create a set of helper methods to make it possible to reuse functionality between the two users. Both users will have to login and join a channel, and both users will have to send messages. Likewise, we can read the chat log from the perspective of either user. So we can factor all of this out into appropriate helper methods:

private void joinChannel(RemoteWebDriver driver, String username, String channel) throws MalformedURLException {
    WebDriverWait wait = new WebDriverWait(driver, 10);
    driver.navigate().to(new URL(CHAT_URL));
    wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("#channel"))).sendKeys(channel);
    driver.findElement(By.cssSelector("#username")).sendKeys(username);
    driver.findElement(By.cssSelector("#joinChannel")).click();
}

private void sendChat(RemoteWebDriver driver, String message) {
    WebDriverWait wait = new WebDriverWait(driver, 10);
    wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("#sendMessageInput"))).sendKeys(message);
    driver.findElement(By.cssSelector("#sendMessageBtn")).click();
}

private String getChatLog(RemoteWebDriver driver) {
    WebDriverWait wait = new WebDriverWait(driver, 10);
    return wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("#messages"))).getText();
}

Note that each of these takes a RemoteWebDriver object, because that is the appropriate superclass of both IOSDriver and AndroidDriver. We don't know within these methods whether we're automating iOS Safari or Android Chrome, and that's a good thing. It keeps the code general.

Now, we can code up the test itself:

@Test
public void testChatApp() throws MalformedURLException {
    joinChannel(safari, USER1_NAME, CHANNEL);
    joinChannel(chrome, USER2_NAME, CHANNEL);
    for (int i = 0; i < USER1_CHATS.size(); i++) {
        sendChat(safari, USER1_CHATS.get(i));
        sendChat(chrome, USER2_CHATS.get(i));
    }
    System.out.println(getChatLog(chrome));
    try { Thread.sleep(4000); } catch (Exception ign) {}
}

As you can see, it's fairly compact. We cause each of the clients to join the same channel, then iterate over their respective list of chat messages, sending each one to the channel. Finally, I retrieve the chat log from one of the users, and print it out to the console. Of course, in an actual test, we could make assertions on the content of the messages box, or even assert appropriate similarity between the chat logs seen by each user. Finally, for visual impact while running the test, we wait for 4 seconds before shutting everything down (again, this would not be the case in an actual test).

Recap

That's really all there is to it! Let me highlight the strategy we've taken here, in a way that could be adapted to any multi-user flow:

  1. Define the number of users required for the test.
  2. In setup, instantiate one Appium driver object for each participating user, taking care to follow all the rules for running Appium sessions in parallel.
  3. In the test itself, interleave commands for the separate drivers, following the logic of an actual user flow (usually these happen serially, since behavior for one user is dependent on behavior for another user, but if possible and appropriate, commands could run simultaneously in different threads).
  4. In teardown, make sure to clean up all the drivers.

Want to have a look at the full code sample for this multi-user flow? Check it out on GitHub! And by all means, let me know if you've implemented any interesting multi-user or multi-device flows with Appium.