Appium を使用した UI テスト

UI テストでは、ボタンのタップ、テキストの入力、ページ間の移動などの操作を自動化することで、アプリのユーザー インターフェイスが正しく動作することを確認します。 単体テストでは個々のメソッドとクラスを分離して検証しますが、UI テストでは、実際のデバイスまたはエミュレーターで完全なアプリを実行し、実際のユーザー操作によってのみ発生する回帰をキャッチします。

Appium は、複数のプラットフォームでネイティブ、ハイブリッド、Web アプリで動作するオープンソースの UI テスト フレームワークです。 Appium はプラットフォーム レベルで動作するため、.NET MAUIまたはプラットフォームネイティブ のツールを使用して構築されたかどうかに関係なく、アプリは同じ方法でテストされます。 Appium では、プラットフォーム固有のドライバーを使用して、アプリに対話を送信します。

プラットフォーム Appium ドライバー ホスト OS
Android UIAutomator2 Windows、macOS、Linux
iOS XCUITest macOS のみ
Mac Catalyst Mac2 macOS のみ
Windows Windows Windowsのみ

注

テストできる内容は、開発用コンピューターによって異なります。 Windowsでは、Android アプリとWindows アプリをテストできます。 macOS では、Android、iOS、Mac Catalyst アプリをテストできます。

ヒント

完全な作業サンプルについては、GitHubの Basic Appium NUnit サンプルを参照してください。

前提条件

UI テストを記述して実行する前に、次の前提条件をインストールします。

Node.js と Appium

Appium は Node.js上に構築されています。 Node.js (LTS バージョンを推奨) をインストールし、Appium とテストするプラットフォームのドライバーをインストールします。

# 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

ターミナルで appium を実行して、インストールを確認します。 サーバーが起動し、ポート 4723 でリッスン中であることを示すメッセージを表示する必要があります。

Windows アプリケーション ドライバー (Windowsのみ)

Appium Windows ドライバーは、Windows アプリケーション ドライバー (WinAppDriver) を使用してWindowsアプリを自動化します。 WinAppDriver バージョン 1.2.1 をダウンロードしてインストールします。

Important

特に WinAppDriver バージョン 1.2.1 を使用してください。 Appium Windows ドライバーでは、他のバージョンが正しく動作しない可能性があります。

Android SDK (Android のみ)

Android SDK がインストールされていること、および ANDROID_HOME 環境変数がその場所を指していることを確認します。 .NET MAUI Android 開発用にマシンを既に設定している場合は、この設定を行う必要があります。

テスト用の.NET MAUI アプリを準備する

テストから操作するすべての UI 要素には、 AutomationId プロパティを一意の値に設定する必要があります。 このプロパティは、Appium が要素の検索に使用するプラットフォーム固有のアクセシビリティ識別子にマップされます。

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

Android アクティビティの登録

Android の場合、Appium はアプリを起動するために完全修飾アクティビティ名を知っている必要があります。 アプリのパッケージ名に一致する値を持つ [Register] 属性を MainActivity クラスに追加します。

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

Register属性の値が、プロジェクト ファイルのApplicationIdとテスト セットアップのAppActivity機能と一致していることを確認します。

テスト プロジェクトを作成する

実証済みのプロジェクト構造では、 NoTargets プロジェクトの共有テスト コードを使用して、プラットフォームごとに個別のテスト プロジェクトを使用します。 これは、非公式のサンプルと.NET MAUIコードベース自体で使用されるのと同じパターンです。

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

各プラットフォーム プロジェクトは、共有プロジェクト内のファイルにリンクすることで、共有コードをコンパイルします。 つまり、共有プロジェクト内のすべてのテストは、テストするすべてのプラットフォームで実行されます。 共有プロジェクト自体を直接実行することはできません。 プラットフォーム固有のテスト プロジェクトのいずれかを常に実行します。

Important

すべてのテスト プロジェクト (たとえば、 UITests) で名前空間を同じにします。 NUnit の [SetUpFixture] 属性は、同じ名前空間内のすべてのテスト フィクスチャに対してセットアップ メソッドを実行します。 名前空間が一致しない場合、テストの実行時に Appium ドライバーは初期化されません。

プラットフォーム テスト プロジェクトの NuGet パッケージ

各プラットフォーム固有のテスト プロジェクトには、次の NuGet パッケージが必要です。

<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>

ヒント

テスト プロジェクトをゼロから作成する代わりに、プロジェクト構造全体をスキャフォールディングするコミュニティで管理されたテンプレートをインストールできます。 詳細については、Template.Maui.UITesting GitHub リポジトリを参照してください。

基本テスト クラスを記述する

共有プロジェクトに BaseTest.cs ファイルを作成します。 この基本クラスは、Appium ドライバーへのアクセスと、Windows上での要素の配置方法と他のプラットフォームとの違いを処理するヘルパー メソッドを提供します。

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));
    }
}

Windowsでは、AutomationId は MobileBy.AccessibilityId 経由でアクセスされます。 Android、iOS、macOS では、 MobileBy.Idを使用します。

Appium サーバーを起動する

テストを実行する前に、Appium サーバーが実行されている必要があります。 最も簡単な方法は、ターミナルから手動で開始することです。

appium

既定では、サーバーは http://127.0.0.1:4723 で起動します。 テスト中はそのまま動かしておいてください。

プログラムでサーバーを起動する (省略可能)

テスト スイートを自己完結型にするには、テスト実行の一環として Appium サーバーを開始および停止できます。 共有プロジェクトに AppiumServerHelper.cs ファイルを作成します。

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();
    }
}

プラットフォームごとに Appium を構成する

各プラットフォーム プロジェクトには、プラットフォーム固有のオプションを使用して Appium ドライバーを構成する AppiumSetup.cs ファイルが含まれています。 このクラスでは、NUnit の [SetUpFixture] 属性を使用して、すべてのテストを実行する前にドライバーを 1 回初期化します。

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();
    }
}

注

Android 上のデバッグ ビルドの場合は、 NoReset を true に設定します。 デバッグ ビルドでは高速展開が使用され、Appium の既定のリセット動作により、高速配置に必要なライブラリが削除されます。 リリース ビルドの場合は、代わりに、 App プロパティを署名された .apk ファイルの完全なパスに設定できます。

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

Mac Catalyst の場合は、 BundleId オプションを設定する必要があります。 これを使用しないと、Appium によってアプリの代わりに Finder が自動化されます。

UI テストを記述する

テスト クラスを共有プロジェクトに追加して、すべてのプラットフォームで実行できるようにします。 各テスト クラスは、 BaseTestから継承する必要があります。 次の例では、既定の.NET MAUI テンプレートのカウンター ボタンをテストします。

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"));
    }
}

FindUIElement メソッドは、AutomationId値によってコントロールを検索します。 他のセレクターを使用することもできます。

Selector メソッド メモ
AutomationId MobileBy.Id() または MobileBy.AccessibilityId() 最寄り。 プラットフォームの違いを抽象化するには、 FindUIElement ヘルパーを使用します。
XPath MobileBy.XPath() 低速ですが、複雑なクエリに役立ちます。
クラス名 MobileBy.ClassName() ネイティブ クラス名で要素を検索します。

ヒント

テストするすべての要素に AutomationId を設定します。 これは、すべてのプラットフォームで要素を見つけるための最も信頼性が高くパフォーマンスの高い方法です。

UI テストの実行

テストを実行する前に、次のことを確認します。

  • .NET MAUI アプリは、ターゲット デバイスまたはエミュレーターにデプロイされます。
  • Android の場合、エミュレーターが起動されているか、物理デバイスが接続されています。
  • iOS の場合、シミュレーターが実行されているか、デバイスがプロビジョニングされています。
  • Windowsの場合、アプリがインストールされます (たとえば、最初Visual Studioから実行するなど)。
  • macOS の場合、 .app バンドルはビルドされ、 AppiumSetup.csで指定されたパスで使用できます。

Test Explorer を使用してVisual Studioからテストを実行するか、コマンド ラインからテストを実行します。

dotnet test UITests.Android/UITests.Android.csproj

テストするプラットフォームに合わせてプロジェクトパスを置き換えます。

こちらも参照ください