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?
First, let's take a look at some common use cases for a multi-user or multi-device test:
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.
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):
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.
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:
It's a simple test, but one that has to involve the coordination of multiple Appium devices and multiple Appium sessions.
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.
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 ImmutableList
s, 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).
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:
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.