tafuji's blog

C#, Xamarin, Azure DevOps を中心に書いています。

Xamarin.UITest Tips for Xamarin.Forms Controls

Qiita 記事 より転載

0. はじめに

この記事は、Xamarin その1 Advent Calendar 2017 - Qiita の 23日目のエントリーです。12月23日は、私の誕生日でして、昨年に引き続き今年も23日目を担当させていただき、嬉しく思います。よろしくお願いいたします。

本記事は、Xamarin.UITest における Tips 集です。Xamarin で提供される個別のコントロールに対するテストコード方法について調査・検証した結果をまとめています。

1. Xamarin UITest の基本

AutomationId 属性

UI テストでは、ある特定のコントロールに対して操作を行う必要があります。Xamarin.Forms のコントロールには、UI テスト時にコントロールを識別するための仕組みとして、AutomationId 属性が提供されています。UI テストで操作する必要のあるコントロールには、AutomationId 属性を付与するようにしましょう。以下に Button コントロールに AutomationId を付与した XAML の例を記載します。

<Button AutomationId="ButtonDemoPage.Button" Command="{Binding ClickCommand}" Text="Click"/>

この AutomadionId は、各プラットフォームで異なるフィールドに割り当てられます。

Platform フィールド名
iOS id
Android label

Repl (read-eval-print-loop) を利用して、各プラットフォームの UI ツリーを確認すると上の表のように AutomationId が割り当てられている様子がわかります。

  • iOS の場合

Automation-Id-iOS

AutomationId-Android

IApp インターフェース

アプリケーションに対する操作(タップ、スワイプなどの操作)は、IApp インターフェースが提供するメソッド経由で行います。以下に主なメソッドを記載します。

メソッド 概要
Query 指定した条件に該当する UI 要素を検索する
Tap UI 要素をタップする
Screenshot スクリーンショットを撮影する
EnterText テキストを入力する
ScrollUp 上にスクロールする
ScrollDown 下にスクロールする
Flash 当該 UI 要素を点滅させる

先ほどの Button コントロールをタップしたい場合は、Xamarin.UITest では以下のように記述します。

app.Tap(x => x.Marked("ButtonDemoPage.Button"));

AppQuery クラス

名前から想像できるように、アプリケーションの UI ツリーに対するクエリを組み立てるためのメソッドを提供するクラスです。

メソッド 概要
Class 引数に指定した型にマッチした UI コントロールを取得する
Marked 引数で指定した識別子やテキスト値にマッチするコントロールを取得する
Child コントロールの子供を取得する
Id 指定した Id に該当するコントロールを取得する
Index UI コントロール配列から当該インデックスの要素を取得する
Invoke ネイティブコントロールのメソッド・プロパティを呼び出す

先ほどの Button コントロールをタップする例 app.Tap(x => x.Marked("ButtonDemoPage.Button")) の意味は、"ButtonDemoPage.Button" という識別子で Mark されたコントロールを取得して、タップするという意味になります。

2. Xamarin.UITest Tips for Xamarin.Forms Controls

ここからは、Xamairn.Forms で提供される各コントロールの Xamarin.UITest の Tips になります。

Label

Label

ラベルの表示が正しいかを Xamarin.UITest で確認をする方法を記載します。XAML 上で以下のように Label が定義されているとします。

<Label AutomationId="LabelDemoPage.Label" Text="Welcome to Xamarin.Forms!"/>

Label コントロールは、iOS では UILabel クラス、Android では TextView クラスのオブジェクトになります。このとき、Xamarin UI Test で Label を取得するには、以下のようにコードを記載します。

app.Query(x => x.Marked("Welcome to Xamarin.Forms!"));

Image

Image

Image コントロールを Xamairn.UITest で取得する方法について記載します。XAML 上に以下のように Image コントロールが定義されているものとします。

<Image AutomationId="ImageDemoPage.Image"
       Source="https://www.xamarin.com/content/images/pages/branding/assets/xamagon.png"/>

以下のコードに記載するように、AutomationId を利用して、Image コントロールを取得することができます。

app.Query(x => x.Marked("ImageDemoPage.Image"));

BoxView

BoxView

ここでは、BoxView コントロールをタップする例を記載します。以下のように XAML 上で BoxView が定義されているとします。

<BoxView x:Name="MyBoxView" AutomationId="FormsGallery.BoxViewDemoPage.BoxView"
         Color="Blue" WidthRequest="150" HeightRequest="150">
    <BoxView.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding TapCommand}"/>
    </BoxView.GestureRecognizers>
</BoxView>

この BoxView を Xamarin.UITest でタップするには、以下のようにコードを記述します。

app.Tap(x => x.Marked("FormsGallery.BoxViewDemoPage.BoxView"));

Button

Button

ボタンをタップするシナリオを Xamarin.UITest で実現する方法を記載します。なお、XAML 上では以下のようにボタンが実装されているとします。

<Button AutomationId="ButtonDemoPage.Button"
        Command="{Binding ClickCommand}"
        Text="Click"/>

Xamarin.UITest でボタンをタップするには IApp.Tap メソッドを利用します。

app.Tap(x => x.Marked("ButtonDemoPage.Button"));

SearchBar

SearchBar

SearchBar で検索条件の入力・検索を Xamarin.UITest で実現する方法を記載します。なお、XAML 上では、以下のように SearchBar が実装されているとします。

<SearchBar AutomationId="SearchBar"
           x:Name="SampleSearchBar"
           Text="{Binding SearchWord}"
           SearchCommand="{Binding SearchCommand}"
           SearchCommandParameter="{Binding SearchWord}"/>

検索条件を入力し、検索処理を実行するには、IApp.EnterText メソッドで条件を入力後、 IApp.PressEnter メソッドを利用します。

app.EnterText(x => x.Marked("SearchBar"), "Title");
app.PressEnter();

検索条件をクリアするには、IApp.CelarText メソッドを実行後、 IApp.DissmissKeyboard メソッドでキーボードを消します。

app.ClearText(x => x.Marked(mark));
app.DismissKeyboard();

検索処理をキャンセルする場合は、AndroidiOS で異なる処理を記述する必要があります。

// SearchBar のキャンセルボタンを押す
if (platform == Platform.Android)
{
    app.Tap(x => x.Id("search_close_btn"));
}
else if (platform == Platform.iOS)
{
    app.Tap(x => x.Marked("Cancel"));
}
app.DismissKeyboard();

Slider

Slider

Slider の値を設定・取得するシナリオを Xamarin.UITest で実現する方法について記載します。以下のように XAML 上に Slider が定義されているとします。

<Slider AutomationId="Slider"
        Minimum="0"
        Maximum="100"
        Value="{Binding Value}"/>
  • 値を設定するには、IApp.SetSliderValue メソッドを利用します
  • 値を取得するには、ネイティブコントロールのメソッドを AppQuery.Invoke で呼び出します
    • Android
      • getProgress メソッド
    • iOS
      • UISlider クラスの value プロパティ
string marked = "SliderDemoPage.Slider";
if(platform == Platform.Android)
{
    var progress = 500.0;
    app.SetSliderValue(marked, progress);
    var actual = Convert.ToDouble(app.Query(x => x.Marked(marked).Invoke("getProgress"))[0]);
}
else if(platform == Platform.iOS)
{
    var progress = 50;
    app.SetSliderValue(marked, progress);
    var actual = Convert.ToInt32(app.Query(x => x.Class("UISlider").Invoke("value"))[0]);
}

Stepper

Stepper

Stepper の値の増減を Xamarin.UITest で実現する方法を記載します。XAML 上に以下のように Stepper に定義されているものとします。

<Stepper Minimum="0" Maximum="100" Value="{Binding Value}"/>

AutomationId を利用して Stepper の値を増減させたいところですが、AutomationId を利用して Stepper の増減ボタンを押すことができません。理由は、AutomationId が、Android では、LinearLayout に付与され、iOS では UIStepper に付与されており、値を増減させるボタンに付与されていないためです。

  • Stepper を Repl の tree コマンド表示した図(Android

Stepper-Repl-Android

  • Stepper を Repl の tree コマンド表示した図(iOS

Stepper-Repl-iOS

従って、各プラットフォームでいかに説明するように Xamarin.UITest のコードを記述する必要があります。

Android の場合

Android の場合は、Stepper のボタンは、ボタンのテキストが、"+" または "-" となるため、当該のボタンをタップするコードを記述します。

app.Tap(x => x.Class("android.widget.Button").Text("+")); // increment
app.Tap(x => x.Class("android.widget.Button").Text("-")); // decrement

iOS の場合

iOS の場合は、Stepper のボタンは、ボタンのテキストが、"Increment" または "Decrement" なので、当該のボタンをタップするコードを記述します。

app.Tap(x => x.Marked("Increment")); // increment
app.Tap(x => x.Marked("Decrement")); // decrement

Switch

Switch

Switch の ON / OFF を切り替える操作と Switch の ON / OFF 状態を取得するシナリオを、Xamarin.UITest で実現する方法について記載します。以下のように XAML 上で Switch が定義されているとします。

<Switch AutomationId="SwitchDemoPage.Switch" IsToggled="{Binding Value}"/>

Switch の ON / OFF を切り替えるには、IApp.Tap メソッドで AutomationId を指定して、Switch をタップします。Switch の ON / OFF の状態を取得するには、ネイティブのメソッド・フィールドを AppQuery.Invoke で呼び出して取得する必要があります。

以下に各プラットフォームで利用すべきネイティブのメソッド・フィールドについて記載します。

プラットフォーム ネイティブコントロール メソッド・フィールド 補足
Android Android.Widget.Switch isChecked メソッドを利用する
iOS UISwitch isOn フィールドの値を取得する ON の時には 1、OFF の時には 0 が返される

Xamarin.UITest のコードは、以下のようになります。

string marked = "SwitchDemoPage.Switch";
app.Tap(x => x.Marked(marked)); // Tap Switch control

if (platform == Platform.Android)
{
    var result = (bool)app.Query(x => x.Marked(marked).Invoke("isChecked"))[0];
}
else if(platform == Platform.iOS)
{
    // If switch is ON, get 1.
    // If switch is OFF, get 0.
    var result = Convert.ToInt32(app.Query(x => x.Marked(marked).Invoke("isOn")));
}

DatePicker

DatePicker

DatePicker で日付を選択するシナリオを Xamarin.UITest で実現する方法について記載します。XAML 上で以下のように DatePicker が定義されているものとします。

<DatePicker AutomationId="DatePickerDemoPage.DatePicker" Format="D"/>

DatePicker での日付の選択を Xamarin.UITest で実現するには、以下のステップで処理を実装する必要があります。

  • DatePicker をタップし、日付選択の Picker を起動する
    • IApp.Tap メソッドに AutomationId を指定してタップ処理を実行します
    • DatePicker が表示されるまで待ちます
  • 日付を変更する
    • Android の場合
      • DatePickerAndroid.Widget.DatePicker のオブジェクトに変換されるので、AppQuery.Invokeメソッドを利用して Android.Widget.DatePicker クラスの updateDate メソッドを呼び出す
    • iOS の場合
      • UIPickerTableView の年、月、日に該当するそれぞれの列に対して、以下の操作を行う必要があります
        • IApp.ScrollDownTo / IApp.ScrollUpTo メソッドを利用してスクロールさせる
        • スクロール後、当該列のテキストをタップして、値を確定する
  • Picker を閉じる
    • Android の場合
      • Android.Widget.DatePicker の OK ボタンは、"button1" という Id が付与されているため、Id から OK ボタンのコントロールを取得して、IApp.Tap メソッドを実行する
    • iOS の場合
      • "Done" とマークされたボタンをタップします

Xamarin.UITest のコードは以下のようになります。テスト対象のデバイスの言語は en-US を想定しています。

var date = new DateTime(2018, 12, 23);
CultureInfo enus = new CultureInfo("en-US");
var month = enus.DateTimeFormat.GetMonthName(date.Month);
var expected = date.ToString("D", enus);
var actual = string.Empty;

var mark = "DatePickerDemoPage.DatePicker";
if (platform == Platform.Android)
{
    app.Tap(x => x.Marked(mark));
    app.WaitForElement(x => x.Class("DatePicker"));
    app.Query(x => x.Class("DatePicker").Invoke("updateDate", date.Year, date.Month -1, date.Day));
    app.Tap(x => x.Id("button1")); //Ok Button in DatePicker Dialogue
}
else if(platform == Platform.iOS)
{
    app.Tap(x => x.Id(mark));
    app.WaitForElement(x => x.Class("UIPickerView"));

    // Scroll DatePicker items
    var iOSTableViewClass = "UIPickerTableView";
    app.ScrollDownTo(z => z.Marked(month), x => x.Class(iOSTableViewClass).Index(0), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Text(month));

    app.ScrollDownTo(z => z.Marked(date.Day.ToString()), x => x.Class(iOSTableViewClass).Index(3), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Text(date.Day.ToString()));

    app.ScrollDownTo(z => z.Marked(date.Year.ToString()), x => x.Class(iOSTableViewClass).Index(6), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Text(date.Year.ToString()));

    app.Tap(x => x.Marked("Done")); // Tap button marked with "Done".
}

iOS の日付の変更ですが、UIDatePickerselectRow() メソッドで値の設定はできるのですが、"Done" ボタンをタップした後に、DatePicker の日付の表示が変更できませんでした。いろいろと試行錯誤をした結果、ここに記載している方法にたどり着きました。selectRow() メソッドを利用した方法でも日付の変更ができるよという情報があれば、教えていただけると幸いです。

TimePicker

TimePicker

TimePicker で時刻を選択するシナリオを、Xamarin.UITest で実現する方法について記載します。XAML 上で以下のように TimePicker が定義されているものとします。

<TimePicker AutomationId="TimePickerDemoPage.TimePicker" Format="T"/>

TimePicker での時刻の選択するシナリオの流れは、DatePicker の流れとほぼ同じです。

  • TimePicker をタップし、日付選択の Picker を起動する
    • IApp.Tap メソッドに AutomationId を指定してタップ処理を実行する
    • TimePicker が表示されるまで待ちます
  • 時刻を変更する
    • Android の場合
      • TimePicker は "timePicker" という Id が付与された Android.Widget.TimePicker のオブジェクトに変換されるので、AppQuery.Invokeメソッドを利用して Android.Widget.TimePicker クラスの setHour メソッド、setMinute を呼び出す
    • iOS の場合
      • UIPickerTableView の時間、分、AM/PM に該当するそれぞれの列に対して、以下の操作を行う必要がある
        • IApp.ScrollDownTo / IApp.ScrollUpTo メソッドを利用してスクロールさせる
        • スクロール後、当該列のテキストをタップして、値を確定する
  • Picker を閉じる
    • Android の場合
      • Android.Widget.TimePicker の OK ボタンは、"button1" という Id が付与されているので、`Id から OK ボタンのコントロールを取得して、IApp.Tap メソッドを実行する
    • iOS の場合
      • "Done" とマークされたボタンをタップする
var hour = 11;
var minutes = 59;
var meridian = "PM";
var mark = "TimePickerDemoPage.TimePicker";

if (platform == Platform.Android)
{
    app.Tap(x => x.Marked(mark));
    app.WaitForElement(x => x.Id("timePicker"));

    app.Query(x => x.Id("timePicker").Invoke("setHour", hour + 12));
    app.Query(x => x.Id("timePicker").Invoke("setMinute", minutes));

    app.Tap(x => x.Id("button1")); //Ok Button in DatePicker Dialogue
}
else if(platform == Platform.iOS)
{
    app.Tap(x => x.Id(mark));
    app.WaitForElement(x => x.Class("UIPickerView"));

    app.ScrollDownTo(z => z.Marked(hour.ToString()), x => x.Class(iOSTableViewClass).Index(0), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Text(hour.ToString()));

    app.ScrollDownTo(z => z.Marked(minutes.ToString()), x => x.Class(iOSTableViewClass).Index(3), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Text(minutes.ToString()));

    app.ScrollDownTo(z => z.Marked(meridian), x => x.Class(iOSTableViewClass).Index(6), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Text(meridian));

    app.Tap(x => x.Marked("Done")); // Tap "Done" button.
}

Entry

Entry

Entry のテキスト入力・クリアを行うシナリオを、Xamarin.UITest で実現する方法について記載します。以下のように EntryXAML 上に定義されているとします。

<Entry  AutomationId="EntryDemoPage.MailEntry" Placeholder="Enter email address"/>

テキストの入力には IApp.EnterText メソッドを利用し、テキストのクリアには IApp.CelarText メソッドを利用します。

var mailId = "EntryDemoPage.MailEntry";

//EnterText
app.EnterText(x => x.Marked(mailId), "someone@somewhere.local");
app.DismissKeyboard();

//ClearText
app.ClearText(x => x.Marked(mailId));
app.DismissKeyboard();

Editor

Editor

Editor の UI テストは、Entry と同様の方法で実装できます。以下のような XAML が定義されているとします。

<Editor AutomationId="EditorDemoPage.Editor"/>

テキストの入力・クリアは以下のように実装することができます。

var marked = "EditorDemoPage.Editor";

//EnterText
app.EnterText(x => x.Marked(marked), "very long text");
app.DismissKeyboard();

//ClearText
app.ClearText(x => x.Marked(marked));
app.DismissKeyboard();

ActivityIndicator

ActivityIndicator

以下の二つのシナリオを Xamarin.UITest で実現する方法について記載します。

  • ActivityIndicator が表示・非表示になるまで待つ
  • インジケーターの表示・非表示を切り替える

なお、ActivityIndicatorXAML で以下のように定義されているものとします。

<ActivityIndicator AutomationId="ActivityIndicator" IsRunning="True"/>

ActivityIndicator が表示・非表示になるまで待つ

特定の要素の表示・非表示を待つので、WaitForElement 又は WaitForNoElement メソッドを利用します。以下にサンプルを示します。

// ActivityIndicator が表示されるまで待つ
app.WaitForElement(x => x.Marked("ActivityIndicator"), timeout: TimeSpan.FromSeconds(30));

// ActivityIndicator が非表示になるまで待つ
app.WaitForNoElement(x => x.Marked("ActivityIndicator"), timeout: TimeSpan.FromSeconds(30));

インジケーターの表示・非表示を切り替える

インジケーターの表示・非表示を切り替えるには、ActivityIndicator が、ネイティブではどのようにレンダリングされているかを知っておく必要があります。以下に Android, iOS それぞれのプラットフォームについて、ActivityIndicatorレンダリングされるコントロール名、インジケーターの表示・非表示を切り替えるメソッドについて記載します。

プラットフォーム ネイティブコントロール 表示・非表示に関連するネイティブメソッド
Android Android.Widget.ProgressBar setVisibility メソッド
iOS UIActivityIndicatorView stopAnimating メソッド

従って、Xamarin UITest でインジケーターの表示・非表示を切り替えるには、上記のネイティブメソッドを AppQuery.Invoke を利用して呼び出します。

if(platform == Platform.Android)
{
    app.Query(x => x.Marked(mark).Invoke("setVisibility", 8));
    app.WaitForNoElement(x => x.Marked(mark), timeout: TimeSpan.FromSeconds(30));
}
else if(platform == Platform.iOS)
{
    app.Query(x => x.Marked(mark).Invoke("stopAnimating"));
    app.WaitForNoElement(x => x.Marked(mark), timeout: TimeSpan.FromSeconds(30));
}

ProgressBar

ProgressBar

ProgressBar の進捗状況を設定・取得するシナリオを Xamarin UITest で実現する方法について記載します。以下のように ProgressBar が XAML 上に定義されているとします。

<ProgressBar AutomationId="ProgressBarDemoPage.ProgressBar"
             VerticalOptions="CenterAndExpand"/>

ProgressBar の進捗状況の取得・設定を行うためには、ProgressBar がネイティブコントロールに対して、AppQuery.Invoke を実行する必要があります。以下に各プラットフォームにおけるコントロール、進捗状況を取得・設定するメソッド・プロパティの対応関係を示します。

プラットフォーム ネイティブコントロール メソッド・プロパティ 補足
Android Android.Widget.ProgressBar setProgress / getProgress メソッド を利用する 0 ~ 10,000の値をとり、10,000 が 100% に対応する
iOS UIProgressView progress プロパティを利用する 0.0 ~ 1.0 の値をとり、1.0f が 100% に対応する

具体的なコードを見ていきます。

if (platform == Platform.Android)
{
    // 進捗を 25% に設定する
    app.Query(x => x.Class("ProgressBar").Invoke("setProgress", 2500));

    // 進捗を取得する
    long progress = (long)app.Query(x => x.Class("ProgressBar").Invoke("getProgress"))[0];
}
else if (platform == Platform.iOS)
{
    // 進捗を 50% に設定する
    app.Query(x => x.Class("UIProgressView").Invoke("setProgress:animated", 0.5));

    // 進捗を取得する
    var progress = app.Query(x => x.Class("UIProgressView").Invoke("progress"))[0];

    // Convert をかけているのは、long 型でデータが返される場合があるため(値が 0 の時など)
    double value = System.Convert.ToDouble(progress);
}

iOS で進捗を取得するときに、long 型でデータが返されることがあるため、Convert クラスを利用して値を変換するなどの処理を行っておく必要があります。

Picker

Picker

Picker の項目を選択するシナリオを Xamarin.UITest で実現する方法について記載します。以下のように Picker が XAML 上に定義されているとします。

<Picker AutomationId="PickerDemoPage.Picker"
        Title="Color"
        ItemsSource="{Binding PickerItems}"
        SelectedItem="{Binding SelectedColor}"/>

UI Test では以下の 3つのステップを実現する必要があります。

  • Picker をタップして選択項目のリストを表示する
    • AutomationId を付与したコントロールIApp.Tap メソッドでタップする
  • 選択対象となる項目が表示されるまで Picker をスクロールさせる
    • IApp.ScrollDownTo 又は IApp.ScrollUpTo メソッドを利用して、タップ対象の項目が表示されるまでスクロールさせる
  • 選択対象の項目をタップする
    • 対象の項目を IApp.Tap メソッドを利用してタップする
  • iOS の場合は、最後に "Done" ボタンをタップする
// Picker をタップする
var marked = "PickerDemoPage.Picker";
app.Tap(x => x.Marked(marked));

if (platform == Platform.Android)
{
    app.ScrollDownTo(z => z.Marked("Yellow"),
                        x => x.Id("select_dialog_listview"),
                        timeout: TimeSpan.FromSeconds(30),
                        strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Marked("Yellow"));
}
else if(platform == Platform.iOS)
{
    app.ScrollUpTo(z => z.Marked("Aqua"),
                    x => x.Class("UIPickerTableView").Index(0),
                    timeout: DefaultTimeout,
                    strategy: ScrollStrategy.Auto);
    app.Tap(x => x.Marked("Aqua"));
    app.Tap(x => x.Marked("Done")); // Done をタップする
}

ListView

ListView

ListView の項目を選択するシナリオを Xamarin.UITest で実現する方法について記載します。XAML 上で以下のように ListView が定義されているものとします。DataTemplate 内のコントロールで、UI Test で取得するコントロールには、AutomationId を付与するようにしましょう。

<ListView AutomationId="ListViewDemoPage.ListView"
            ItemsSource="{Binding People}"
            IsPullToRefreshEnabled="True"
            RefreshCommand="{Binding RefleshCommand}"
            IsRefreshing="{Binding IsRefleshing, Mode=TwoWay}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell AutomationId="ListViewDemoPage.ViewCell">
                <StackLayout Orientation="Horizontal" Margin="1,1">
                    <BoxView  Color="{Binding FavoriteColor}"/>
                    <StackLayout Orientation="Vertical">
                        <Label AutomationId="ListViewDemoPage.NameLabel"
                                Text="{Binding Name}"/>
                        <Label AutomationId="ListViewDemoPage.BirthDayLabel"
                                Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}"/>
                    </StackLayout>
                </StackLayout>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
  • 画面上に表示されているアイテムは、IApp.Query メソッドを利用して、 AutomationId から取得することができます
  • 画面上に表示されていないアイテム(リストの最後尾のアイテムなど)を取得したい場合は、IApp.ScrollDownTo 等を利用して画面をスクロール後、アイテムの取得を行う必要があります

以下に Xamarin.UITest のサンプルコードを示します。

var mark = "ListViewDemoPage.ListView";
var nameLabelMark = "ListViewDemoPage.NameLabel";

app.ScrollDownTo(z => z.Marked(nameLabelMark).Text("Yvonne"), x => x.Marked(mark), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);

var nameLabels = app.Query(x => x.Marked(nameLabelMark));
var birthdayLabels = app.Query(x => x.Marked("ListViewDemoPage.BirthDayLabel"));

Pull To Refresh が実装された ListView の UI Test

ListView を引っ張って更新するシナリオを Xamarin.UITest で実現したい場合の補足説明を記載します。具体的には、以下の二つの操作を実装する方法について説明します。

  • ListView を引っ張って更新する
  • ListView が更新中かどうかを判定する

ListView を引っ張って更新するには、ListView をドラッグすればよいことになります。ここでは、ListView最初のセルの中心座標から、ListView の中心座標までをドラッグすることで実現します。

ListView-PullToRefresh

以下に Xamarin.UITest のサンプルを示します。

AppResult firstCellInList = null;
if(platform == Platform.Android)
{
    firstCellInList = app.Query(x => x.Class("ViewCellRenderer_ViewCellContainer").Index(0)).FirstOrDefault();
}
else if (platform == Platform.iOS)
{
    firstCellInList = app.Query(x => x.Class("Xamarin_Forms_Platform_iOS_ViewCellRenderer_ViewTableCell")).FirstOrDefault();
}

var firstCenterX = firstCellInList.Rect.CenterX;
var firstCenterY = firstCellInList.Rect.CenterY;

var listview = app.Query(x => x.Marked(marked))[0];
var listviewCenterX = listview.Rect.CenterX;
var listviewCenterY = listview.Rect.CenterY;

app.DragCoordinates(firstCenterX, firstCenterY, listviewCenterX, listviewCenterY);

ListView が更新中かどうかを判定する方法は、AndroidiOS それぞれのネイティブの知識が必要になります。

Platform ネイティブコントロール 判定方法
Android android.support.v4.widget.SwipeRefreshLayout isRefreshing メソッドを利用する
iOS UIRefreshControl コントロールが存在するかどうかで判定する

判定を行う処理のサンプルを以下に示します。

var isRefreshing = false;
if (platform == Platform.Android)
{
    isRefreshing = (bool)app.Query(x => x.Class("SwipeRefreshLayout").Invoke("isRefreshing")).First();
}
else(platform == Platform.iOS)
{
    isRefreshing = (bool)app.Query(x => x.Class("UIRefreshControl")).Any();
}

この判定処理を定期的にチェックすることで、ListView が更新中かどうかを判定することができます。 以下のように判定処理をプロパティ化し、判定処理を一定時間リトライするメソッドを実装し、WaitForIndicatorToDisappear(3, 10) のように待ちの処理を実装するとよいでしょう。

public bool RefreshIndicatorIsDisplayed
{
    get
    {
        if (platform == Platform.Android)
            return (bool)app.Query(x => x.Class("SwipeRefreshLayout").Invoke("isRefreshing")).First();

        if (platform == Platform.iOS)
            return (bool)app.Query(x => x.Class("UIRefreshControl")).Any();

        throw new Exception("NotSupportedPlatform");
    }
}

public void WaitForIndicatorToDisappear(int retryCount = 3, int waitSeconds = 10)
{
    int counter = 0;
    while (RefreshIndicatorIsDisplayed)
    {
        Thread.Sleep(waitSeconds * 1000);
        counter++;

        if (counter >= retryCount)
            throw new Exception($"待ち時間 {waitSeconds * retryCount} をオーバーしました");
    }
}

TableView

TableView

TableView 内のセルを操作するシナリオを、Xamarin.UITest で実現する方法について記載します。以下のように XAML 上に TableView が定義されているものとします。

<TableView  AutomationId="TableViewFormDemoPage.TableView" Intent="Form">
    <TableSection Title="Table Section">
        <TextCell AutomationId="TextCell" Detail="With Detail Text"  Text="Text Cell"/>
        <ImageCell AutomationId="ImageCell" Text="Image Cell"
                   ImageSource="https://www.xamarin.com/content/images/pages/branding/assets/xamagon.png"/>
        <SwitchCell AutomationId="SwitchCell" Text="Switch Cell" IsEnabled="False"/>
        <EntryCell AutomationId="EntryCell" Label="Entry Cell" Placeholder="Type text here"/>
        <ViewCell AutomationId="ViewCell">
            <StackLayout>
                <Label AutomationId="ViewCell.Label" Text="A ViewCell can be anything you want!"/>
            </StackLayout>
        </ViewCell>
    </TableSection>
</TableView>

TableView コントロールの取得は、AutomationId を指定して取得することができます。しかしながら、TableView 内のセルnには、XAMLAutomationId を付与してもネイティブのオブジェクトツリーには反映されません。従って、TableView のコントロールを取得した後に、Child メソッドを駆使しながら、テスト対象のコントロールを取得することになります。

以下、Xamairn.Forms で提供されている標準のセルコントロールについて、Xamarin.UITest ではどのように実装したらよいかについて記載します。

TextCell

TextCell を Repl で評価すると、各プラットフォームで以下のように変換されていることがわかります。

[ConditionalFocusLayout]
[TextCellRenderer_TextCellView > LinearLayout]
    [TextView] text: "Text Cell"
    [TextView] text: "With Detail Text"
[View]
  • iOS の場合
[Xamarin_Forms_Platform_iOS_CellTableViewCell] text: "Text Cell"
  [UITableViewCellContentView]
    [UITableViewLabel] label: "Text Cell",  text: "Text Cell"
    [UITableViewLabel] label: "With Detail Text",  text: "With Detail Text"
  [_UITableViewCellSeparatorView]
  [_UITableViewCellSeparatorView]
  [UITableTextAccessibilityElement] label: "Text Cell, With Detail Text"

TextCell コントロールを取得するには、表示されている文字列を指定してコントロールを取得します。

// TextCell
var textCellText = app.Query(x => x.Marked("Text Cell"));
var textCellDetail = app.Query(x => x.Marked("With Detail Text"));

EntryCell

EntryCell のテキストの入力・クリアを行うシナリオを、Xamarin.UITest で実現する方法について記載します。

Android の場合、EntryCell を Repl で評価すると以下のようなツリー構造になっていることがわかります。

[ConditionalFocusLayout]
[EntryCellView]
    [TextView] text: "Entry Cell"
    [EntryCellEditText]
[View]

Android の場合、入力用のテキストボックスは、EntryCellView クラスの子オブジェクトの EntryCellEditText オブジェクトであることがわかります。従って、EntryCell でテキストを入力するには以下の方針でテキストを入力する必要があります。

  • AppQuery.Class メソッドで EntryCellView クラスのオブジェクトを取得し、その子オブジェクトを取得する
    • 子オブジェクトを取得する方法は、以下の二通りの方法がある
      • AppQuery.Child メソッドでインデックスを指定する
      • AppQuery.Child メソッドでクラス名(EntryCellEditText)を指定する
  • テキストの入力は IApp.EnterText メソッド、テキストのクリアは IApp.ClearText メソッドを利用する

iOS の場合、EntryCell を Repl で評価すると以下のようなツリー構造になっていることがわかります。

[Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell] text: "Entry Cell"
  [UITableViewCellContentView]
    [UITextField]
      [UITextFieldLabel] label: "Type text here",  text: "Type text here"
      [_UITextFieldContentView > UITextSelectionView]
      [UIAccessibilityTextFieldElement] text: "Type text here"
    [UITableViewLabel] label: "Entry Cell",  text: "Entry Cell"
  [_UITableViewCellSeparatorView]
  [UITableTextAccessibilityElement] label: "Entry Cell"
  [UIAccessibilityElementMockView] text: "Type text here"
    [UIAccessibilityTextFieldElement] text: "Type text here"

iOS の場合、入力用のテキストボックスは、Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell クラスの孫(UITableViewCellContentView クラスの子供)となっているので、Xamarin.UITest では、以下の方針でテキストの入力・クリアの処理を実装する必要があります。

  • AppQuery.Class メソッドで Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell を取得し、その最初の子供を取得する
    • 子オブジェクトは、Child メソッドで取得する
      • インデックス指定か、クラス名(UITableViewCellContentView)を指定する
    • その子供の最初のコントロールChild メソッドで取得する
      • インデックス指定か、クラス名
  • テキストの入力は IApp.EnterText メソッド、テキストのクリアは IApp.ClearText メソッドを利用する

以下に、サンプルコードを示します。

// Enter text.
var inputText = "This is text for test.";
if(platform == Platform.Android)
{
    app.EnterText(x => x.Class("EntryCellView").Child(1), inputText);
    app.EnterText(x => x.Class("EntryCellView").Child("EntryCellEditText"), inputText);

    app.DismissKeyboard();

}
else if (platform == Platform.iOS)
{
    app.EnterText(x => x.Class("Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell").Child(0).Child(0), inputText);
    app.EnterText(x => x.Class("Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell").Child("UITableViewCellContentView").Child("UITextField"), inputText);
    app.DismissKeyboard();
}

// Clear text.
if (platform == Platform.Android)
{
    app.ClearText(x => x.Class("EntryCellView").Child(1));
}
else if (platform == Platform.iOS)
{
    app.ClearText(x => x.Class("Xamarin_Forms_Platform_iOS_EntryCellRenderer_EntryCellTableViewCell").Child(0).Child(0));
}

SwitchCell

SwitchCellSwitch の ON / OFF を切り替えるシナリオを、Xamarin.UITest で実現する方法について記載します。

Android の場合、SwitchCell を Repl で評価すると、次のようなツリー構造になっています。従って、SwitchCellView クラスの子供の Switch クラスのオブジェクトに対して、Invoke メソッドを呼び出してやればよいことになります。

[ConditionalFocusLayout]
[SwitchCellView]
    [LinearLayout]
    [TextView] text: "Switch Cell"
    [Switch]
[View]

iOS の場合も Repl で評価すると以下のようなツリー構造になっているため、Xamarin_Forms_Platform_iOS_CellTableViewCell クラスの子供の UISwitch クラスのオブジェクトに対して、Invoke メソッドを使用して、ON / OFF を切り替えればよいことになります。

[Xamarin_Forms_Platform_iOS_CellTableViewCell] text: "Switch Cell"
  [UITableViewCellContentView]
    [UITableViewLabel] label: "Switch Cell",  text: "Switch Cell"
  [UISwitch > UISwitchModernVisualElement] label: "Switch Cell",  text: "0"
    [UIView > UIView]
    [UIView > UIView]
    [UIView > UIImageView]
    [UIImageView]
  [_UITableViewCellSeparatorView]
  [UITableTextAccessibilityElement] label: "Switch Cell",  text: "0"
  [UIAccessibilityElementMockView] label: "Switch Cell",  text: "0"

以下に、サンプルコードを記載します。

// SwitchCell
if (platform == Platform.Android)
{
    app.Query(x => x.Class("SwitchCellView").Class("Switch").Invoke("setChecked", true));
}
else if (platform == Platform.iOS)
{
    app.Query(x => x.Class("Xamarin_Forms_Platform_iOS_CellTableViewCell").Class("UISwitch").Invoke("setOn:animated", true));
}

ImageCell

ImageCell に配置された Image を Xamarin.UITest で取得する場合を考えます。

Android の場合のオブジェクトツリーは以下のようになるため、TextCellRenderer_TextCellView クラスの子供の `ImageView オブジェクトを取得することになります。

[ConditionalFocusLayout]
[TextCellRenderer_TextCellView]
    [ImageView]
    [LinearLayout]
    [TextView] text: "Image Cell"
[View]

iOS の場合のオブジェクト構造は以下のようなツリー構造となるため、Xamarin_Forms_Platform_iOS_CellTableViewCell クラスの孫となる UIImageView オブジェクトを取得することになります。

[Xamarin_Forms_Platform_iOS_CellTableViewCell] text: "Image Cell"
  [UITableViewCellContentView]
    [UITableViewLabel] label: "Image Cell",  text: "Image Cell"
    [UIImageView]
  [_UITableViewCellSeparatorView]
  [UITableTextAccessibilityElement] label: "Image Cell"

以下にサンプルコードを記載します。

// ImageCell
if (platform == Platform.Android)
{
    app.Query(x => x.Class("TextCellRenderer_TextCellView").Class("ImageView"));
}
else if(platform == Platform.iOS)
{
    app.Query(x => x.Class("Xamarin_Forms_Platform_iOS_CellTableViewCell").Child("UITableViewCellContentView").Class("UIImageView"));
}

ViewCell

ViewCell の Xamarin.UITest のコードは、セル内に配置したコントロールに依存します。ここで記載している各コントロールのテストコードの書き方を参考にしてください。

Frame

Frame

Frame コントロールを Xamarin.UITest で取得する方法について記載します。XAML 上に以下のように Frame が定義されているものとします。

<Frame AutomationId="FrameDemoPage.Frame"
       OutlineColor = "Black"
       VerticalOptions = "CenterAndExpand">
    <Label Text = "I've been framed!"/>
</Frame>

AutomationId を指定して以下のようなコードで、Frame コントロールを取得することができます。

app.Query(x => x.Marked("FrameDemoPage.Frame"));

NavigationPage

NavigationPage において、前の画面に戻る場合は、IApp.Back メソッドを利用しましょう。

app.Back();

MasterDetailPage

MasterDetailPage

以下のシナリオを Xamarin.UITest で実現方法について記載します。

  • ハンバーガーメニューをタップする
  • メニューのアイテムを選択する

ハンバーガーメニューをタップする

ハンバーガーメニューをタップするには、ネイティブでどのようなコントロールに変換されているかを知っておく必要があります。

<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  xmlns:local="clr-namespace:FormsGallery"
                  x:Class="FormsGallery.MasterDetailPageDemoPage"
                  Title="Master Detail"
                  IsPresented="{Binding IsPresented}">
    <MasterDetailPage.Master>
        <local:ColorListPage/>
    </MasterDetailPage.Master>
    <MasterDetailPage.Detail>
        <NavigationPage>
            <x:Arguments>
                <local:NamedColorPage Title="Detail"/>
            </x:Arguments>
        </NavigationPage>
    </MasterDetailPage.Detail>
</MasterDetailPage>

Repl で UI のツリー構造を調べると、ハンバーガーメニューは、ネイティブでは、AppCompatImageButton の "OK" ボタンに変換されていることがわかります。

[Toolbar] id: "toolbar"
    [AppCompatTextView] text: "Detail"
    [AppCompatImageButton] label: "OK"
  • iOS の場合

Repl で UI のツリー構造を調べると、ハンバーガーメニューは、ネイティブでは label にアイコン名が付与されたボタンに変換されていることがわかります。

[UINavigationBar] id: "Detail"
[_UIBarBackground > UIImageView]
[_UINavigationBarContentView]
    [_UIButtonBarStackView]
    [_UIButtonBarButton > ... > UIImageView] label: "hamburger"
    [UILabel] label: "Detail",  text: "Detail"
[_UIButtonBarButton > ... > UIImageView] label: "hamburger"

従って、ハンバーガーメニューをタップするには、各プラットフォームで以下のように Xamarin.UITest のコードを記述する必要があります。

プラットフォーム タップする方法 補足
Android AppCompatImageButton ボタンで、ボタンのテキストが "OK" のボタンを IApp.Tap メソッドでタップする
iOS メニューの label の値でコントロールを取得し、 IApp.Tap メソッドをタップする label は、Title の値。ただし、Icon を指定したときはアイコン名(拡張子を除く)

以下にハンバーガーメニューをタップするコードのサンプルを示します。

if(platform == Platform.Android)
{
    app.Tap(x => x.Class("AppCompatImageButton").Marked("OK"));
}
else if (platform == Platform.iOS)
{
    app.Tap(x => x.Marked("hamburger")); // Tap control marked with icon name
}

メニューの項目を選択する

メニューの項目を選択は、ListView のリストから項目を選択する方法と同じアプローチで実現することができます。

  • 選択対象となる項目が表示されるまで、IApp.ScrollDownTo / IApp.ScrollUpTo メソッドを利用してスクロールする
  • 選択対象となる項目を IApp.Tap メソッドを利用してタップする- メニューのページ

以下のような ListViewXAML 上に定義されており、この ListView に表示される項目をタップする場合を考えます。

<ListView AutomationId="ColorListPage.ListView" ItemsSource="{Binding ColorList}">
    <ListView.Behaviors>
        <behaviorsPack:SelectedItemBehavior Command="{Binding SelectedColorCommand}"/>
    </ListView.Behaviors>
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <StackLayout VerticalOptions="Center">
                    <Label  AutomationId="ColorListPage.ColorLabel" Text="{Binding Name}" Margin="5,0,0,0"/>
                </StackLayout>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

ListView をスクロールし、項目を選択するサンプルコードは、以下のようになります。

app.ScrollDownTo(x => x.Marked("ColorListPage.ColorLabel").Text("Yellow"), x => x.Marked("ColorListPage.ListView"), timeout: TimeSpan.FromSeconds(30), strategy: ScrollStrategy.Auto);
app.Tap(x => x.Marked("ColorListPage.ColorLabel").Text("Yellow"));

TabbedPage

TabbedPage

TabbedPage 内のページを切り替える操作を、Xamarin.UITest で実現する方法について記載します。XAML 上で、以下のように TabbedPage の子ページが定義されているとします。

<TabbedPage.Children>
    <local:Page1 Title="Tab1"/>
    <local:Page2 Title="Tab2"/>
    <local:Page3 Title="Tab3"/>
</TabbedPage.Children>

UITest で Tab2 のコンテンツに切り替えるには、以下のように AppQuery.Marked メソッドで Tab のTitle をコンテンツを取得し、IApp.Tap メソッドを利用してタップ操作を実現することができます。

app.Tap(x => x.Marked("Tab2"));

CarouselPage

CarouselPage

CarouselPage 内に定義されたコンテンツをスワイプ操作で切り替えるシナリオを、Xamarin.UITest で実現する方法について記載します。IApp インタフェースには、スワイプ操作を実現するメソッドが提供されていますので、これを利用することができます。

メソッド 説明
SwipeRightToLeft 右から左へスワイプする
SwipeLeftToRight 左から右へスワイプする

右から左にスワイプ操作を行うコードの例を以下に記載します。

app.SwipeRightToLeft(); // Swipe

ToolbarItems

ToolbarItems

ToolbarItems コントロールのアイテムをタップするシナリオを、Xamairn.UITest で実現する方法について記載します。以下のように XAML 上に ToolBarItems が定義されているものとします。

<ContentPage.ToolbarItems>
    <ToolbarItem AutomationId="ToolbarItemDemoPage.Item1"
                    Text="ToolBar1"
                    Command="{Binding ClickCommand}"
                    CommandParameter="ToolBar1"/>
    <ToolbarItem AutomationId="ToolbarItemDemoPage.Item2"
                    Text="ToolBar2"
                    Command="{Binding ClickCommand}"
                    CommandParameter="ToolBar2"/>
</ContentPage.ToolbarItems>

ToolBarItem はネイティブでは以下のコントロールに変換されます。

プラットフォーム ネイティブコントロール
Android Android.Widget.Button|AutomationId は付与されない
iOS UIBarButtonItem|AutomationIdId プロパティに付与される

したがって、各プラットフォームで IApp.Tap に受け渡すパラメータが異なります。

  • Android
    • ボタンのテキストの文字列を AppQuery.Marked に渡してコントロールを取得し、IApp.Tapメソッドでタップします
  • iOS
    • AutomationIdAppQuery.Id に渡してコントロールを取得し、IApp.Tap メソッドでタップします

サンプルコードを以下に示します。

if (platform == Platform.Android)
{
    app.Tap(x => x.Marked("ToolBar1"));
    app.Tap(x => x.Marked("ToolBar2"));
}
else if(platform == Platform.iOS)
{
    app.Tap(x => x.Id("ToolbarItemDemoPage.Item1"));
    app.Tap(x => x.Id("ToolbarItemDemoPage.Item2"));
}

3. おわりに

最後まで、みていただきありがとうございました。以下のコントロールに関する Tips は、時間の都合でここに掲載できなかったのですが、いずれアップデートできればと考えています。

  • WebView
  • Map

4. ソースコード

記事の動作検証に利用したソースコードは、Github に公開しています。