Testing web & mobile app interaction with Selenium & Appium

 

Sometimes there is a need to test the way two different apps interact or work together.

Say you have a mobile app for a coffee shop. When you place your order with the app, a notice shows up on the order terminal for the barista.  They can then fulfill the order and have it ready for you — no waiting in line.

As a customer
I want to place an order for coffee on my iPhone
So that it's ready when I get to the coffee shop

But if they can’t fulfill the order (maybe they’re out of caramel-soy-macchi-whatever mix) they can let the customer know so that they can cancel or place a new order without waiting to find out that their order is not available.

As a barista
I want to notify customers when their order can't be fulfilled
So that they can change or cancel their order

There are two different apps here, and two different actors.  This can make the test challenging.  The obvious solution is to automate both apps at the same time (the mobile app for the customer and the web-based point of sale terminal for the barista.

Your test might looks something like this:


public void testCustomerDeviceAndBaristaTerminalTogether()
{
// Create two sessions in one test — one on a browser for the barista and one on a mobile device for the customer
browser = new RemoteWebDriver(seleniumServerURL, browserCapabilites);
phone = new AppiumDriver(appiumServerURL, deviceCapabilities);
// Barista logs into terminal app in browser
browser.get(baristaApp.loginPage.url);
browser.findElement(baristaApp.loginPage.usernameField).sendKeys(barista.username);
browser.findElement(baristaApp.loginPage.passwordField).sendKeys(barista.password);
browser.findElement(baristaApp.loginPage.loginButton).click();
// Customer logs into mobile app on mobile device
phone.findElement(mobileApp.loginScreen.usernameField).sendKeys(customer.username);
phone.findElement(mobileApp.loginScreen.passwordField).sendKeys(customer.password);
phone.findElement(mobileApp.loginScreen.loginButton).click();
// Customer selects a coffee — after login succeeded
coffeeButton = phoneWait.until(ExpectedConditions.elementToBeClickable(mobileApp.menuScreen.coffeeButton));
coffeeButton.click();
// Barista receives the order and marks it out of stock
browserWait.until(ExpectedConditions.visibilityOfElementLocated(baristaApp.orderDialog));
browser.findElement(baristaApp.orderDialog.outOfStockButton).click();
// Customer is prompted to cancel or place new order
orderConfirmationDialog = phoneWait.until(ExpectedConditions.visibilityOfElementLocated(mobileApp.orderConfirmationDialog));
// User receives out of stock notification and can cancel or place new order
assertThat(orderConfirmationDialog.getText()).contains("Out of Stock");
assertThat(orderConfirmationDialog.findElement(By.accessibilityId("Cancel Order"))).exists();
assertThat(orderConfirmationDialog.findElement(By.accessibilityId("Place New Order"))).exists();
}

The problem here is that coordination between the two apps can be tricky.  Synchronization and timing isn’t guaranteed and I’m not sure if the explicit waits will always handle this.

Also, it requires standing up both environments and making sure that the mobile app can communicate with your web app.  It can get tricky.  Not to mention it will be inherently slower and the odds of random failures increases.

Another thing you can do is test the majority of use cases independently.  This is hinted at by our two stories above.  One for the barista (web app) and a separate one for the customer (mobile app.)

Unless you have a really unique architecture, it’s likely that the two apps don’t actually know anything about each other.  They probably communicate through web services with a shared back end database or message queue.

Really, what you want to do is test each app independently and how it interacts with the service.  The service can be mocked or stubbed for some use cases, but for end-to-end tests, it makes sense to use the service.

So your test will now look something like this:


public void testCustomerDeviceWithBaristaApi()
{
// Create a session for the customer app
phone = new AppiumDriver(sauceURL, deviceCapabilities);
// Create an API service that interacts with the mobile app
baristaAPI = new BaristaAPI(barristUsername, baristaPassword);
// Customer logs into mobile app on mobile device
phone.findElement(mobileApp.loginScreen.usernameField).sendKeys(customer.username);
phone.findElement(mobileApp.loginScreen.passwordField).sendKeys(customer.password);
phone.findElement(mobileApp.loginScreen.loginButton).click();
// Customer selects a coffee — after login succeeded
coffeeButton = phoneWait.until(ExpectedConditions.elementToBeClickable(mobileApp.menuScreen.coffeeButton));
coffeeButton.click();
// API finds the customer order and sets it to out of stock
order = baristaApi.findOrdersForCustomer(customer.username).get(0);
baristaApi.markOrderAsOutOfStock(order);
// Customer is prompted to cancel or place new order
orderConfirmationDialog = phoneWait.until(ExpectedConditions.visibilityOfElementLocated(mobileApp.orderConfirmationDialog));
// User receives out of stock notification and can cancel or place new order
assertThat(orderConfirmationDialog.getText()).contains("Out of Stock");
assertThat(orderConfirmationDialog.findElement(By.accessibilityId("Cancel Order"))).exists();
assertThat(orderConfirmationDialog.findElement(By.accessibilityId("Place New Order"))).exists();
}

This requires a clear API and may require some backend interaction that is not normally exposed.  But the test is much cleaner (and reliable) and if exposed services require additional security you can have a separate test API endpoint or authorization token that enables the additional functionality.  In this case, that shouldn’t be necessary.

You may still want to perform a few end-to-end sanity tests to make sure the environments are communicating correctly and are compatible, but the number of these tests can be greatly reduced — and the speed and reliability of your test suite improved.

Leave a comment