Writing UI tests for Xamarin is awesome and easy using Xamarin.UITest, which allows you to write tests in C# that work across both iOS and Android. It provides a nice API to interact with your apps from test code, allowing for performing many common tasks for interacting with an app such as tapping elements, entering text, scrolling, swiping, etc.
Typical UI Tests
Using a tip calculator app as an example, here's what one such test might look like:
[Test]
public void BrittleAndSadTest()
{
app.EnterText("SubTotal", "100.00");
app.EnterText("TipPercentage", "20");
var tipText = app.Query("TipAmount").First().Text;
var totalText = app.Query("Total").First().Text;
Assert.AreEqual("Tip: $20.00", tipText);
Assert.AreEqual("Total: $120.00", totalText);
}
The test enters values in the text fields for sub total and tip percentage, queries the resulting tip amount and total out of the UI, and then asserts the values are correct. Pretty awesome, right? With just a few lines of code we're verifying the expected behavior from the app from the same perspective as a user, ensuring the app is producing the expected results.
The Problem
One problem with this test is that in addition to validating the behavior of the app, it also has knowledge of how to query the values out of the UI. This is a very common pitfall in UI tests on any platform, and can lead to very brittle tests that start failing quickly as the UI evolves and the tests fall out of date.
All too often I've seen this result in failing tests being ignored as they become incompatible with updated interfaces, and ultimately they get abandoned completely. Thankfully, there's a good solution to this that you should keep in mind right from the start when writing UI tests.
Screen Object Pattern
If you're familiar with writing automated tests for the web using libraries such as Selenium you might already be familiar with the Page Object pattern. This is a pattern that works on any UI platform, and is not limited to just web applications. When working with mobile applications I personally like to refer to this as the Screen Object pattern, but it's still effectively the same thing so feel free to use whichever term you prefer.
The basic idea is that instead of each test having deep knowledge of the UI and how to query it, you define classes that represent each screen and expose the appropriate methods and properties to interact with it. In this model, the screen class is the single place that needs to know how to find elements in the UI so if things change around and the queries need to be updated, you only need to update them in one place. On the other end, consumers of the class (e.g. the tests) get a nice typesafe API for interacting with these screens. This results in more readable and maintainable tests in the end.
Refactored Tests
Let's take a look at what a screen object might look like for this screen:
class TipScreen
{
private readonly IApp _app;
public TipScreen(IApp app)
{
_app = app;
}
public void EnterSubTotal(decimal subTotal)
{
_app.EnterText("SubTotal", subTotal.ToString());
}
public void EnterTipPercentage(int percentage)
{
_app.EnterText("TipPercentage", percentage.ToString());
}
public string TipText => _app.Query("TipAmount").First().Text;
public string TotalText => _app.Query("Total").First().Text;
}
There's not much magic here, which is really the point. The queries for interacting with the UI are all contained here, and exposed are simple methods for interacting with the screen. You can also see that the .ToString()
calls on sub total and tip percentage are encapsulated inside of this class as well, so the exposed methods provide a nice typesafe API.
With this screen object in place we can update the test to make use of it:
[Test]
public void DurableAndAwesomeTest()
{
var screen = new TipScreen(app);
screen.EnterSubTotal(100);
screen.EnterTipPercentage(20);
Assert.AreEqual("Tip: $20.00", screen.TipText);
Assert.AreEqual("Total: $120.00", screen.TotalText);
}
Not only is the test more durable and resistant to change, it's also far more readable now. Instead of being polluted with all the noise of UI queries, it now clearly shows the scenario being tested and the expected results of it.
If you're writing any sort of UI tests, even very simple ones, I highly encourage you to start following this pattern. It makes it easier to write more tests and keep your apps stable and covered, and vastly improves the odds of you either losing a lot of time to keeping them up to date as your UI evolves, or even worse, ultimately abandoning them entirely.
Happy testing!