Walkthough: Creating UI Tests for Xamarin.Forms Apps, Part 2

In Part 1, we created a File -> New Xamarin.Forms app, and added UI tests to it.

In this post we'll learn how to create scalable UI tests that allow us to reuse code using Page Object architecture.

We'll take an existing app, and add a Xamarin.UITest project using the Page Object architecture.

Why Page Object Architecture?

I know what you're thinking: "I already have UI tests written. Why would I want to go back and rewrite all of that code?".

The goal of the Page Object architecture is to reuse as much code as possible

Check out how much duplicate code exists between these two UI tests:

[Test]
public void UpdateFirstName()
{
    app.Screenshot("App Started");
   
    app.WaitForElement(x => x.Marked("Armstead, Evan"));
    app.Screenshot("List Displayed");
    
    app.Tap(x => x.Marked("Armstead, Evan"));
    app.Screenshot("Evan Details Displayed");
    
    app.Tap(x => x.Marked("Edit"));
    app.Screenshot("Edit Screen Displayed");
    
    app.ScrollDownTo("First");
    app.Tap(x => x.Marked("Evan"));
    app.ClearText();
    app.Screenshot("Cleared first name field");
    
    app.EnterText("Jonathan");
    app.DismissKeyboard();
    app.Screenshot("Entered Jonathan");
    
    app.Tap(x => x.Marked("Save"));
    app.Screenshot("First Name Changed");
}
[Test]
public void UpdateLastName()
{
    app.Screenshot("App Started");
   
    app.WaitForElement(x => x.Marked("Armstead, Evan"));
    app.Screenshot("List Displayed");
    
    app.Tap(x => x.Marked("Armstead, Evan"));
    app.Screenshot("Evan Details Displayed");
    
    app.Tap(x => x.Marked("Edit"));
    app.Screenshot("Edit Screen Displayed");
    
    app.ScrollDownTo("Last");
    app.Tap(x => x.Marked("Armstead"));
    app.ClearText();
    app.Screenshot("Cleared Last Name");
    
    app.EnterText("Smith");
    app.DismissKeyboard();
    app.Screenshot("Entered Smith");
    
    app.Tap(x => x.Marked("Save"));
    app.Screenshot("Last Name Changed");
}

There's at least 8 lines of code that are exactly the same bewteen these two tests!

As we write more and more tests like these, the amount of duplicated code continues to grow.

Then, if we decide to refresh the UI, we have to change the same duplicated line of code multiple times, and it becomes a maintenance nightmere!

Enter: Page Object Architecture.

What is Page Object Architecture?

In the Page Object Architecture, we will create a class for each page in our app. Each page class will contain all of the actions that a user can perform on the page, e.g. tapping a button, entering text, etc.

Using Page Object Architecture, our tests now reuse code and become more easily readable! Take a look at how the same tests look using Page Object Architecture:

[Test]
public void UpdateFirstName()
{
    ContactsListPage.WaitForListToLoad();
    ContactsListPage.TapContact("Armstead, Evan");

    ContactDetailsPage.TapEdit();
    ContactDetailsPage.ClearFirstName();

    ContactDetailsPage.EnterFirstName("Jonathan");
    ContactDetailsPage.TapSaveButton();
}
[Test]
public void UpdateLastName()
{
    ContactsListPage.WaitForListToLoad();
    ContactsListPage.TapContact("Armstead, Evan");

    ContactDetailsPage.TapEdit();
    ContactDetailsPage.ClearLastName();

    ContactDetailsPage.EnterLastName("Smith");
    ContactDetailsPage.TapSaveButton();
}

Implementing Page Object Architecture

Quick Note: Before writing any UI tests, make sure you've assigned an AutomationId to each UI control in your app. I recommend creating a unique public const string for each AutomationId, and putting all of the string constants into a Shared Project that both your Xamarin.Forms project and UI test project can reference. This makes it super easy to reference each AutomationId in our UI test. Here's an example of how I've implemented it for today's project.

1. Add a UI Test Project

Right-click on the Solution and select Add -> New Project

Select Multiplatform -> Tests -> UI Test App

Name the UITest Project [Solution].UITests

2. Update and Install NuGet Packages

Update Xamarin.UITest NuGet Package

Install Xamarin.TestCloud.Agent NuGet Package into the iOS Project
(Why? The UI Test Automation APIs on iOS are private, the Xamarin.TestCloud.Agent NuGet Package will expose the APIs. Android's automation APIs are already public, so nothing extra needs to be added to the Android project.)

Expose iOS's Automation APIs by calling Xamarin.Calabash.Start() in AppDelegate.FinishedLaunching.

Wrap it in the compiler attribute #if DEBUG to ensure that your submission to the iOS App Store does not have iOS Automation APIs enabled becuase otherwise Apple will reject your app:

The Calabash assembly makes uses of non-public Apple API's which will cause apps to be rejected by the App Store

[Register(nameof(AppDelegate))]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions)
    {
        global::Xamarin.Forms.Forms.Init();
        EntryCustomReturn.Forms.Plugin.iOS.CustomReturnEntryRenderer.Init();

#if DEBUG
        Xamarin.Calabash.Start();
#endif

        LoadApplication(new App());

        return base.FinishedLaunching(uiApplication, launchOptions);
    }
}

3. Add BasePage.cs

Add a new .cs file to the UITests project called BasePage.cs

public abstract class BasePage
{
    protected BasePage(IApp app, string pageTitle)
    {
        App = app;
        Title = pageTitle;
    }

    public string Title { get; }
    protected IApp App { get; }

    public virtual void WaitForPageToLoad() => App.WaitForElement(Title);
}

4. Create Page Class

Add a new .cs file to the UITests project called [YourPageName]Page.cs. In this example, I am creating PersonListPage.cs.

First, add this custom using statement so that we don't have to repeatedly type System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>

using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;

Next, make sure your page class inhereits from BasePage. Here's what my PersonListPage class looks like so far:

using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;

namespace CosmosDbSampleApp.UITests
{
    public class PersonListPage : BasePage
    {
    }
}

In the page class, let's add a Query field for each UI control on the page. Here's what my PersonListPage class looks like so far:

using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;

namespace CosmosDbSampleApp.UITests
{
    public class PersonListPage : BasePage
    {
        readonly Query _personList, _activityIndicator, _addButton;
    }
}

Now let's initialize the Query fields in the constructor using x => x.Marked("[Control's Automation Id]");. Here's what my PersonListPage looks like so far:

using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;

namespace CosmosDbSampleApp.UITests
{
    public class PersonListPage : BasePage
    {
        readonly Query _personList, _activityIndicator, _addButton;

        public PersonListPage(IApp app, string pageTitle) : base(app, pageTitle)
        {
            _personList = x => x.Marked(AutomationIdConstants.PersonListPage_PersonList);
            _activityIndicator = x => x.Marked(AutomationIdConstants.PersonListPage_ActivityIndicator);
            _addButton = x => x.Marked(AutomationIdConstants.PersonListPage_AddButton);
        }
    }
}

Lastly, let's add a public method for each action a user can do on the page. For example, if a user can tap a button on the page, let's create a method for that. Here's the final code for my PersonListPage class:

using Xamarin.UITest;

using CosmosDbSampleApp.Shared;

using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;

namespace CosmosDbSampleApp.UITests
{
    public class PersonListPage : BasePage
    {
        readonly Query _personList, _activityIndicator, _addButton;

        public PersonListPage(IApp app, string pageTitle) : base(app, pageTitle)
        {
            _personList = x => x.Marked(AutomationIdConstants.PersonListPage_PersonList);
            _activityIndicator = x => x.Marked(AutomationIdConstants.PersonListPage_ActivityIndicator);
            _addButton = x => x.Marked(AutomationIdConstants.PersonListPage_AddButton);
        }

        public void TapAddButton()
        {
            App.Tap(_addButton);
            App.Screenshot("Add Button Tapped");
        }

        public void WaitForActivityIndicator() 
        {
            App.WaitForElement(_activityIndicator);
            App.Screenshot("Activity Indicator Appeared");
        }

        public void WaitForNoActivityIndicator()
        {
            App.WaitForNoElement(_activityIndicator);
            App.Screenshot("Activity Indicator Disappeared");
        }
    }
}

Repeat this step and create a page class for each page in the app.

5. Add BaseTest.cs

Add a new .cs file to the UITests project called BaseTest.cs

public abstract class BaseTest
{
    readonly Platform _platform;

    protected BaseTest(Platform platform) => _platform = platform;

    protected IApp App { get; private set; }

    [SetUp]
    public virtual void TestSetup() => App = AppInitializer.StartApp(_platform);
}

In BaseTest.cs, create a property with a for each page class using the following format:
protected [Page Class Type] [Page Object Name] { get; private set; }

Here's how my BaseTest.cs looks so far:

public abstract class BaseTest
{
    readonly Platform _platform;

    protected BaseTest(Platform platform) => _platform = platform;

    protected IApp App { get; private set; }
    protected PersonListPage PersonListPage { get; private set; }
    protected AddPersonPage AddPersonPage { get; private set; }

    [SetUp]
    public virtual void TestSetup()
    {
        App = AppInitializer.StartApp(_platform);
    }
}

Now, in TestSetup(), let's initialize each page object. Here's how my BaseTest.cs looks now:

public abstract class BaseTest
{
    readonly Platform _platform;

    protected BaseTest(Platform platform) => _platform = platform;

    protected IApp App { get; private set; }
    protected PersonListPage PersonListPage { get; private set; }
    protected AddPersonPage AddPersonPage { get; private set; }

    [SetUp]
    public virtual void TestSetup()
    {
        App = AppInitializer.StartApp(_platform);
        
        PersonListPage = new PersonListPage(App, PageTitles.PersonListPage);
        AddPersonPage = new AddPersonPage(App, PageTitles.AddPersonPage);
    }
}

That's it for our BastTest!

6. Create UI Tests

Finally! Now it's time to write our UI tests!

Let's modify Test.cs to inherit from BaseTest, and remove unneeded code that came from the template:

using System.Linq;

using NUnit.Framework;

using Xamarin.UITest;

namespace CosmosDbSampleApp.UITests
{
    [TestFixture(Platform.Android)]
    [TestFixture(Platform.iOS)]
    public class Tests : BaseTest
    {
        public Tests(Platform platform) : base(platform)
        {
        }
    }
}

Now let's write our first test!

NUnit will recognize any public void method that contains the [Test] attribute.

I've added a Test called public void AddNewContact(). Take a look at my code below to understand how to customize your Test.

ProTip: Use //Arrange //Act //Assert comments to ensure your tests follow best-practices and good organization.

ProTip: NUnit also allows us to use public Task and public async Task methods if we need to run asynchronous code.

using System.Linq;

using NUnit.Framework;

using Xamarin.UITest;

namespace CosmosDbSampleApp.UITests
{
    [TestFixture(Platform.Android)]
    [TestFixture(Platform.iOS)]
    public class Tests : BaseTest
    {
        public Tests(Platform platform) : base(platform)
        {
        }

        [Test]
        public void AddNewContact()
        {
            //Arrange
            const string name = "Test Contact";
            const int age = 37;

            //Act
            PersonListPage.TapAddButton();

            AddPersonPage.EnterName(name);
            AddPersonPage.EnterAge(age);
            AddPersonPage.TapSaveButton();

            PersonListPage.WaitForPageToLoad();

            //Assert
            Assert.IsTrue(App.Query(name).Any());
            Assert.IsTrue(App.Query(age.ToString()).Any());
        }
    }
}