Edit

UI testing with Appium

UI testing verifies that your app's user interface behaves correctly by automating interactions such as tapping buttons, entering text, and navigating between pages. While unit tests validate individual methods and classes in isolation, UI tests exercise the full app on a real device or emulator to catch regressions that only surface through actual user interaction.

Appium is an open-source UI testing framework that works with native, hybrid, and web apps across multiple platforms. Because Appium operates at the platform level, it tests your app the same way regardless of whether it was built with .NET MAUI or platform-native tooling. Appium uses platform-specific drivers to send interactions to your app:

Platform Appium driver Host OS
Android UIAutomator2 Windows, macOS, Linux
iOS XCUITest macOS only
Mac Catalyst Mac2 macOS only
Windows Windows Windows only

Note

What you can test depends on your development machine. On Windows, you can test Android and Windows apps. On macOS, you can test Android, iOS, and Mac Catalyst apps.

Tip

For a complete working sample, see the Basic Appium NUnit Sample on GitHub.

Prerequisites

Before you can write and run UI tests, install the following prerequisites:

Node.js and Appium

Appium is built on Node.js. Install Node.js (LTS version recommended), then install Appium and the drivers for the platforms you want to test:

# Install Appium
npm install -g appium

# Install platform drivers (install only the ones you need)
appium driver install uiautomator2     # Android
appium driver install xcuitest         # iOS (macOS only)
appium driver install mac2             # Mac Catalyst (macOS only)
appium driver install windows          # Windows

Verify the installation by running appium in a terminal. The server should start and display a message indicating it's listening on port 4723.

Windows Application Driver (Windows only)

The Appium Windows driver uses Windows Application Driver (WinAppDriver) to automate Windows apps. Download and install WinAppDriver version 1.2.1.

Important

Use WinAppDriver version 1.2.1 specifically. Other versions may not work correctly with the Appium Windows driver.

Android SDK (Android only)

Make sure the Android SDK is installed and that the ANDROID_HOME environment variable points to its location. If you've already set up your machine for .NET MAUI Android development, this should be in place.

Prepare the .NET MAUI app for testing

All UI elements that you want to interact with from your tests need to have the AutomationId property set to a unique value. This property maps to platform-specific accessibility identifiers that Appium uses to locate elements.

<Button
    x:Name="CounterBtn"
    AutomationId="CounterBtn"
    Text="Click me"
    SemanticProperties.Hint="Counts the number of times you click"
    Clicked="OnCounterClicked"
    HorizontalOptions="Fill" />

Android activity registration

For Android, Appium needs to know the fully qualified activity name to launch your app. Add a [Register] attribute to your MainActivity class with a value that matches your app's package name:

[Register("com.companyname.myapp.MainActivity")]
public class MainActivity : MauiAppCompatActivity
{
}

Make sure the value in the Register attribute matches the ApplicationId in your project file and the AppActivity capability in your test setup.

Create the test projects

A proven project structure uses separate test projects per platform, with shared test code in a NoTargets project. This is the same pattern used in the official sample and the .NET MAUI codebase itself:

MySolution/
├── MauiApp/                  # Your .NET MAUI app
├── UITests.Shared/           # Shared test code (NoTargets project)
├── UITests.Android/          # Android-specific setup
├── UITests.iOS/              # iOS-specific setup
├── UITests.Windows/          # Windows-specific setup
└── UITests.macOS/            # macOS-specific setup

Each platform project compiles the shared code by linking to the files in the Shared project. This means all tests in the Shared project run on every platform you test. The Shared project itself can't be run directly. Always run one of the platform-specific test projects.

Important

Keep the namespace the same across all test projects (for example, UITests). NUnit's [SetUpFixture] attribute runs setup methods for all test fixtures in the same namespace. If the namespaces don't match, the Appium driver won't be initialized when your tests run.

Platform test project NuGet packages

Each platform-specific test project needs the following NuGet packages:

<ItemGroup>
    <PackageReference Include="Appium.WebDriver" Version="8.0.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
    <PackageReference Include="NUnit" Version="3.14.0" />
    <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

Tip

Instead of creating test projects from scratch, you can install community-maintained templates that scaffold the full project structure for you. For more information, see the Template.Maui.UITesting GitHub repository.

Write a base test class

Create a BaseTest.cs file in the Shared project. This base class provides access to the Appium driver and a helper method that handles a difference in how elements are located on Windows versus other platforms:

using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;

namespace UITests;

public abstract class BaseTest
{
    protected AppiumDriver App => AppiumSetup.App;

    protected AppiumElement FindUIElement(string id)
    {
        if (App is WindowsDriver)
        {
            return App.FindElement(MobileBy.AccessibilityId(id));
        }

        return App.FindElement(MobileBy.Id(id));
    }
}

On Windows, AutomationId is accessed through MobileBy.AccessibilityId. On Android, iOS, and macOS, use MobileBy.Id.

Start the Appium server

The Appium server must be running before you execute tests. The simplest approach is to start it manually from a terminal:

appium

The server starts on http://127.0.0.1:4723 by default. Leave it running while you run your tests.

Start the server programmatically (optional)

To make your test suite self-contained, you can start and stop the Appium server as part of the test run. Create an AppiumServerHelper.cs file in the Shared project:

using OpenQA.Selenium.Appium.Service;

namespace UITests;

public static class AppiumServerHelper
{
    private static AppiumLocalService? _appiumLocalService;

    public const string DefaultHostAddress = "127.0.0.1";
    public const int DefaultHostPort = 4723;

    public static void StartAppiumLocalServer(
        string host = DefaultHostAddress,
        int port = DefaultHostPort)
    {
        if (_appiumLocalService is not null)
            return;

        var builder = new AppiumServiceBuilder()
            .WithIPAddress(host)
            .UsingPort(port);

        _appiumLocalService = builder.Build();
        _appiumLocalService.Start();
    }

    public static void DisposeAppiumLocalServer()
    {
        _appiumLocalService?.Dispose();
    }
}

Configure Appium for each platform

Each platform project contains an AppiumSetup.cs file that configures the Appium driver with platform-specific options. This class uses NUnit's [SetUpFixture] attribute to initialize the driver once before all tests run.

Android

using NUnit.Framework;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;
using OpenQA.Selenium.Appium.Enums;

namespace UITests;

[SetUpFixture]
public class AppiumSetup
{
    private static AppiumDriver? driver;

    public static AppiumDriver App => driver
        ?? throw new NullReferenceException("AppiumDriver is null");

    [OneTimeSetUp]
    public void RunBeforeAnyTests()
    {
        AppiumServerHelper.StartAppiumLocalServer();

        var androidOptions = new AppiumOptions
        {
            AutomationName = "UIAutomator2",
            PlatformName = "Android",
        };

        // For debug builds, use NoReset to preserve Fast Deployment libraries
        androidOptions.AddAdditionalAppiumOption(
            MobileCapabilityType.NoReset, "true");
        androidOptions.AddAdditionalAppiumOption(
            AndroidMobileCapabilityType.AppPackage,
            "com.companyname.myapp");
        androidOptions.AddAdditionalAppiumOption(
            AndroidMobileCapabilityType.AppActivity,
            "com.companyname.myapp.MainActivity");

        driver = new AndroidDriver(androidOptions);
    }

    [OneTimeTearDown]
    public void RunAfterAnyTests()
    {
        driver?.Quit();
        AppiumServerHelper.DisposeAppiumLocalServer();
    }
}

Note

For debug builds on Android, set NoReset to true. Debug builds use Fast Deployment, and Appium's default reset behavior deletes the libraries that Fast Deployment requires. For release builds, you can instead set the App property to the full path of the signed .apk file.

iOS

using NUnit.Framework;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.iOS;

namespace UITests;

[SetUpFixture]
public class AppiumSetup
{
    private static AppiumDriver? driver;

    public static AppiumDriver App => driver
        ?? throw new NullReferenceException("AppiumDriver is null");

    [OneTimeSetUp]
    public void RunBeforeAnyTests()
    {
        AppiumServerHelper.StartAppiumLocalServer();

        var iOSOptions = new AppiumOptions
        {
            AutomationName = "XCUITest",
            PlatformName = "iOS",
            PlatformVersion = "17.0",
            DeviceName = "iPhone 15 Pro",
            App = "com.companyname.myapp",
        };

        driver = new IOSDriver(iOSOptions);
    }

    [OneTimeTearDown]
    public void RunAfterAnyTests()
    {
        driver?.Quit();
        AppiumServerHelper.DisposeAppiumLocalServer();
    }
}

Windows

using NUnit.Framework;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;

namespace UITests;

[SetUpFixture]
public class AppiumSetup
{
    private static AppiumDriver? driver;

    public static AppiumDriver App => driver
        ?? throw new NullReferenceException("AppiumDriver is null");

    [OneTimeSetUp]
    public void RunBeforeAnyTests()
    {
        AppiumServerHelper.StartAppiumLocalServer();

        var windowsOptions = new AppiumOptions
        {
            AutomationName = "windows",
            PlatformName = "Windows",
            App = "com.companyname.myapp_9zz4h110yvjzm!App",
        };

        driver = new WindowsDriver(windowsOptions);
    }

    [OneTimeTearDown]
    public void RunAfterAnyTests()
    {
        driver?.Quit();
        AppiumServerHelper.DisposeAppiumLocalServer();
    }
}

Mac Catalyst

using NUnit.Framework;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Enums;
using OpenQA.Selenium.Appium.Mac;

namespace UITests;

[SetUpFixture]
public class AppiumSetup
{
    private static AppiumDriver? driver;

    public static AppiumDriver App => driver
        ?? throw new NullReferenceException("AppiumDriver is null");

    [OneTimeSetUp]
    public void RunBeforeAnyTests()
    {
        AppiumServerHelper.StartAppiumLocalServer();

        var macOptions = new AppiumOptions
        {
            AutomationName = "mac2",
            PlatformName = "Mac",
            App = "/path/to/MyApp/bin/Debug/net10.0-maccatalyst/maccatalyst-x64/MyApp.app",
        };

        macOptions.AddAdditionalAppiumOption(
            IOSMobileCapabilityType.BundleId,
            "com.companyname.myapp");

        driver = new MacDriver(macOptions);
    }

    [OneTimeTearDown]
    public void RunAfterAnyTests()
    {
        driver?.Quit();
        AppiumServerHelper.DisposeAppiumLocalServer();
    }
}

Important

For Mac Catalyst, you must set the BundleId option. Without it, Appium will automate Finder instead of your app.

Write UI tests

Add test classes to the Shared project so they run on all platforms. Each test class should inherit from BaseTest. The following example tests the counter button in the default .NET MAUI template:

using NUnit.Framework;

namespace UITests;

public class MainPageTests : BaseTest
{
    [Test]
    public void AppLaunches()
    {
        App.GetScreenshot().SaveAsFile($"{nameof(AppLaunches)}.png");
    }

    [Test]
    public void ClickCounterTest()
    {
        // Arrange
        var element = FindUIElement("CounterBtn");

        // Act
        element.Click();
        Task.Delay(500).Wait();

        // Assert
        App.GetScreenshot().SaveAsFile($"{nameof(ClickCounterTest)}.png");
        Assert.That(element.Text, Is.EqualTo("Clicked 1 time"));
    }
}

The FindUIElement method locates controls by their AutomationId value. You can also use other selectors:

Selector Method Notes
AutomationId MobileBy.Id() or MobileBy.AccessibilityId() Preferred. Use the FindUIElement helper to abstract the platform difference.
XPath MobileBy.XPath() Slower, but useful for complex queries.
Class name MobileBy.ClassName() Finds elements by their native class name.

Tip

Set AutomationId on every element you want to test. It's the most reliable and performant way to locate elements across all platforms.

Run UI tests

Before running tests, make sure:

  • The .NET MAUI app is deployed to the target device or emulator.
  • For Android, an emulator is booted or a physical device is connected.
  • For iOS, a Simulator is running or a device is provisioned.
  • For Windows, the app is installed (for example, by running it from Visual Studio first).
  • For macOS, the .app bundle is built and available at the path specified in AppiumSetup.cs.

Run tests from Visual Studio using Test Explorer, or from the command line:

dotnet test UITests.Android/UITests.Android.csproj

Replace the project path with the platform you want to test.

See also