Xamarin.UITest を試す (Visual Studio で) (Xamarin.Forms で)

Xamarin.UITest を使ってみました。ただし Windows の Visual Studio で、 Xamarin.Forms で、 Android *1 です。だいぶターゲットが狭くなってしまいました。

*1 iOS のテストを実行しようとすると `iOS tests are not supported on Windows.` というエラーになります。

## 公式ドキュメント

公式ドキュメントはこのあたりだと思います。

- <del>[Xamarin.UITest](https://developer.xamarin.com/guides/testcloud/uitest/) - [Xamarin Developer Center](https://developer.xamarin.com)</del>
- [Xamarin.UITest](https://docs.microsoft.com/ja-jp/appcenter/test-cloud/uitest/) - [Visual Studio App Center](https://docs.microsoft.com/appcenter)

## 始めるまで

### ソリューションに UITest プロジェクトを追加

1. ソリューションを右クリック > 追加 > プロジェクト
1. **Cross-Platform** カテゴリ または **Test** カテゴリの中から **UI Test App (Xamarin.UITest | Cross-Platform)** プロジェクトを追加する

### UITest プロジェクトからテスト対象となる Droid プロジェクトを参照

1. UITest プロジェクトの参照を右クリック > 参照を追加
1. テスト対象となる Droid プロジェクトをチェックして追加

### AppInitializer クラスの StartApp メソッドを記述

1. `ConfigureApp.Android()` と `StartApp()` の間に `InstalledApp("MY_PACKAGE_NAME")` を挿入
1. 同様に `EnableLocalScreenshots()` も挿入

`MY_PACKAGE_NAME` は Droid プロジェクトを右クリック > プロパティ > Android Manifest > Package name の文字列です。

なお、公式は `InstalledApp()` ではなく、 `ApkFile()` で .apk ファイルを指定する方法を推しているようです。<del>定かではないのですが、 `ApkFile()` は Release ビルドしか扱えないっぽいのが難点で。 Release ビルドは本番環境的なところにつながってしまうので、 Debug の方でテストしたい場合が多いんですよね... </del>`.InstalledApp("MY_PACKAGE_NAME")` の代わりに、 `.ApkFile("../../../MyApp.Droid/bin/Debug/com.example.myapp.apk")` としても大丈夫です。

### テスト対象となる Droid プロジェクトを配置する

- 右クリック > 配置

以後、 Droid プロジェクトを変更したら配置し直します。

### 最初のテスト実行

1. テスト エクスプローラーを開きます。(テスト > ウィンドウ > テスト エクスプローラー)

1. テスト エクスプローラーにテスト項目が出ていない場合はもう一度ソリューションをビルドしてください。

1. 実行します。

スクリーンショットを撮るだけのテストがデフォルトで用意されているはずです。とりあえずこれを育てていきます。

```Tests.cs
[Test]
public void AppLaunches()
{
  app.Screenshot("First screen.");
}
```

テストを追加するには、新しくメソッドを作り、 `[Test]` をつけます。テスト プロジェクトをビルドすればテスト エクスプローラーに現れます。

## テストの記述

今作っているアプリの全画面を一通り表示させることを目指してみました。

### AutomationId 指定

```LoginPage.xaml
<Entry
  x:Name="UserName"
  AutomationId="username_entry" />
```

```Tests.cs
app.EnterText("username_entry", "ynakamura@sample.com");
```

こんなふうにビューに **AutomationId** を付けて識別することを公式は推奨しているようです。でも、テストのために既存の XAML に手を入れるのはなるべく避けたいと思って...

### 入力

入力するビューを識別するには、そのページの入力の何番目というふうに指定するのが楽です。

```c#
app.ClearText(c => c.TextField().Index(1));
app.EnterText(c => c.TextField().Index(1), "password");
```

### タップ

タップするボタンを識別するには、表示テキストを指定するのが楽ですし、テストのコードを見たときにわかりやすいと思います。

```c#
app.Tap(c => c.Text("ログイン"));
```

### タブ移動

タブ移動は `app.SwipeRightToLeft();` 等でもいいですし、上の応用で `Text()` でタブ識別して `Tap()` でもいけます。

### DisplayAlert の OK ボタン

**DisplayAlert** の OK ボタンのように **AutomationId** を埋め込めないやつも `Text()` ならいけるのです。汎用性高い :)

```c#
app.Tap(c => c.Text("OK"));
```

### Switch

**Switch** には専用のメソッドが用意されています。1ページに複数の **Switch** が並んでいるときは `Index()` で識別します。

```c#
app.Tap(c => c.Switch().Index(0));
```

### Class 指定

テキストを持たないものは `Class()` でクラス名を指定するという方法があります。

**Image**　をタップします。

```c#
app.Tap(c => c.Class("FormsImageView").Index(0)); // Image
```

**SearchBar** で検索します。

```c#
app.EnterText(c => c.Class("SearchView"), "テスト"); // SearchBar
app.PressEnter();
```

**ListView** のアイテムをタップするには `Child()` を併せて使います。

```c#
app.Tap(c => c.Class("ListView").Child().Index(0)); // ListViewItem
```

### Navbar関連

左上のハンバーガー ボタンをタップします。戻る矢印ボタンに変わっても同じコードでタップできます。Android にはせっかくバック ボタンがあるので、戻るのは `app.Back();` でもいいですね。 _ハンバーガーボタンのクラスが、いつからか分かりませんが、変わったようです。_

```c#
//app.Tap(c => c.Class("ImageButton").Index(0));  // ハンバーガー ボタン
app.Tap(c => c.Class("AppCompatImageButton").Index(0));
```

ToolbarItem の secondary をタップするには、リスト展開と項目選択の二段階です。項目の選択には `Text()` が使えます。

```c#
app.Tap(c => c.Class("OverflowMenuButton"));  // 展開
app.Tap(c => c.Text("更新"));
```

### 関数化

よく使う処理をまとめて関数にして呼び出すこともできます。

```c#
public void Login(string username, string password)
{
  app.ClearText(c => c.TextField().Index(0));
  app.EnterText(c => c.TextField().Index(0), username);
  app.ClearText(c => c.TextField().Index(1));
  app.EnterText(c => c.TextField().Index(1), password);
  app.Tap(c => c.Text("ログイン"));
}

[Test]
public void Hoge()
{
  Login("ynakamura@example.com", "password");
  //...
}
```

## トラブルシューティング

### コードを直したはずなのに直ってない

私の場合はたいてい**配置**を忘れています。

### Found 2 connected Android devices. で失敗する

Adb Server を再起動してください。 (ツール > Android > Restart Adb Server)

### Class で指定するクラス名が分からない

私はテストに次のようなコードを埋め込んで、表示中の要素を列挙して調べました。

```c#
foreach (var item in app.Query(c => c.All()))
  Console.WriteLine(item.Class + "/" + item.Id + "/" + item.Label + "/" + item.Text);
```

### AppQuery は c で受けるの?

c 派と q 派と、あと x 派が少しいるみたいです。

### UITest のログファイルに `Potential Android SDK location: (No path) - Not set. [ Source: ANDROID_HOME ]` と出て失敗する

Windows の場合、システムの詳細設定で次の環境変数を追加し、

|変数|値|
|---|---|
|ANDROID_HOME|C:\Program Files (x86)\Android\android-sdk|

Path に次の値を追加します。

- %ANDROID_HOME%\tools
- %ANDROID_HOME%\platform-tools

### `Detected problems with API compatibility (visit g.co/dev/appcompat for more info)` というメッセージが表示されて失敗する (2018-11-01追記)

[Android P で non-SDK と呼ばれる変更があり、非公開のメソッドやプロパティを利用できなくなったのだとか](https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces)。 Xamarin.UITest のバージョンを _2.2.5_ 以降に上げてください。

> [Xamarin.UITest 2.2.5](https://www.nuget.org/packages/Xamarin.UITest/2.2.5)
>
> * [Android] Android P support

---