How do you practice TDD for Android development projects?

TDD is more of a strategy then a certain process or tool. The idea is that testing drives your development — writing tests first, making sure tests pass to indicate development is complete, and continuously testing on each change to make sure no regressions or new bugs slip in.

You can use Espresso, Robotium, or UIAutomator directly for automating the mobile app but testing the UI is inherently slow, can be brittle, and may not be possible (or easy) to write UI (or end-to-end) tests while an app is under development. The UI may not be testable, or back end services may not be complete at early stages.

With test driven development, you want to use your tests to inform what you develop. This informs what you should develop first, and it helps you to write your application in a way that is testable.

If you have some feature that needs tested — for example: delivering different size media (images & video) based on available bandwidth and screen size, testing this with the UI seems to make sense, since it is a UI feature.

But try writing your test the way you want it to look, not the way it actually behaves in the app. Start with your assertion:

assertThat(imageWidth).isEqualTo(deviceScreenWidth);

Now we try to satisfy that

First we need to get our values. Where does deviceScreenWidth come from? How do we determine imageWidth?

imageWidth is probably sent to the response processor so that when it sends the image URL it resizes the image — or selected the appropriately sized image.

That’s a design decision that’s already being influence by our tests. Maybe we want standard sizes — small, medium, large instead of trying to support every possible pixel width. Maybe isEqualTo should test within a range instead of just equal.

For deviceScreenWidth we need some representation of our device that includes it’s screen size. Do we get it from the userAgent or does the device send DisplayMetrics via an API? Is it passed from a service or a lookup table? Maybe we need a test of the function that passes a device identifier from the userAgent and calculates based on known values.

Now we know what code to write — and another test to write.

This can be a bit of a rabbit hole, but we don’t have to tackle everything at once.

In our unit test we just need to have an imageWidth and a deviceScreenWidth. We can make a note of what functions and parameters are needed to get this information, but for now we can just implement the functions immediately needed — and even make our first test pass by having those functions return hard coded values.

A nice simple test might look like this:

public void testImageCalculator() 
{
    device = new DeviceMetaData(SAMSUNG_GALAXY_S6);
    deviceScreenWidth = device.getDisplayMetrics(device).screen.width;
    imageWidth = getImageSizeForDevice(deviceScreenWidth);
    assertThat(imageWidth).isBetween(deviceScreenWidth, mediumDeviceMaxWidth);
}

Now we know what we need to develop next — the functions that make this test pass. A DeviceMetaData container class, something that gets display metrics for the device, and what we really care about (at this time) — the getImageSizeForDevice() function.

NOTE: This was originally an answer to a question on Quora

 

 

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.