【Flutter】MaestroでUIテストに入門

# テストを書こう！

riverpod で状態管理を実装したアプリのユニットテストの続きです。今回は簡単なUIテストを実装します。

https://qiita.com/Seo-4d696b75/items/eee020162d0537fdbc36


:::note warn
本記事ではローカル実行を原則とし、CI/CDでのリモート実行に関しては割愛します
::::

# Maestroとは？

https://maestro.mobile.dev/

簡単にUIテストを作成できるモバイル向けのテストフレームワーク、もしくはCLIとして提供されている同名の`maestro`コマンドの名前です。Flutter以外にも以下のプラットフォームに対応しています。

- iOS（simulatorのみサポート）
- Android
- Flutter
- ReactNative

後述のFlowと呼ばれるyaml形式のファイルにテストを記述し、`mastro`コマンドで実行します。

## Mastroの導入

公式docに従って`maestro`コマンドを導入します

https://maestro.mobile.dev/getting-started/installing-maestro

基本的には以下のコマンドひとつで完結。upgradeする場合も同じです。

```bash
curl -Ls "https://get.maestro.mobile.dev" | bash
```

初回インストール時はパスを通すのも忘れずに

```bash
export PATH="$PATH":"$HOME/.maestro/bin"
```

iOSシミュレーターを使う場合は以下のツールも必要です

```bash
brew tap facebook/fb
brew install facebook/fb/idb-companion
```

# テスト対象のアプリを用意

:::note info
公式docsではCUI操作が紹介されていますが、通常の開発時のようにGUIのRunボタンからビルド＆インストールしても問題ありません。ビルドモードがDebugであり、Flowファイルで指定するappIdが合っていればOKです。
:::

以下はCUIの操作例です。

-----------

まずAndroid, iOSそれぞれで .apk .app ファイルのビルドが必要です（fvmを使う場合は適宜読み替えてください）。

```bash
# Android
flutter build apk --debug
# iOS
flutter build ios --debug --simulator
```

このとき、UIテスト向けに通常とは異なるフレーバー（mockなど）を指定してビルドする場合もあります。下記は一例ですので各自の開発環境に応じてビルドしてください。

```bash
# エントリーポイントの切り替え
flutter build apk --debug -t lib/main_mock.dart
# dart-define-from-fileの使用
flutter build apk --debug --dart-define-from-file=flavor/mock.json
```


次にビルドしたアプリを端末にインストールして準備完了です。

```bash
# Android端末・エミュレータのシリアル名一覧を確認
adb devices
# インストール
adb -s ${デバイスのシリアル名} install build/app/outputs/flutter-apk/app-debug.apk

# iOSシミュレーターのデバイス一覧を確認
xcrun simctl list devices
# インストール
xcrun simctl install ${device} build/ios/iphonesimulator/Runner.app
```

# テストの用意

Maestro では Flow と呼ばれるyaml形式で記述されたテストの定義ファイルを用意します。

使用できるコマンド一覧・詳細は公式リファレンスを参照してください。

https://maestro.mobile.dev/api-reference/commands

```yaml:（例）基本的な操作と検査コマンド
appId: ${テスト対象のappId}
----
- launchApp # アプリを起動する
- assertVisible: "text" # 指定した文字列の表示を確認する
- tapOn: "TextFiled-label" # フォーカスを当てる
- eraseText # 現在の入力を削除
- inputText: "input" # TextFiledに入力
- tapOn: "Go!" # 指定した文字列部分をタップする
- waitForAnimationToEnd # アニメーションが終わるまで待機する
- scrollUntilVisible: #　指定した文字列が表示されるまで下方向にスクロールする
    element:
      text: "target"
``` 

## Flow記述のTips

いくつかハマりポイントがあったので紹介

### TextFieldへの入力

最初に`TextField.decoration.labelText` で指定した文字列を  `tapOn` でタップしてフォーカスを当てておく必要があります。ラベルが不在の場合は後述の`Semantics`を利用します。また新しい入力で上書きする場合は `eraseText`で現在の値を削除するのも忘れずに。

### Semanticsの利用

`Text`のような目に見える文字列が不在で`tapOn`のターゲットを上手く指定できない場合は、`Semantics`ウェイジェットでラベルを指定できます。`tapOn: ".*my-label.*"`

```dart
Semantics(
    label: "my-label",
    child: MyWidget()
)
```

### Androidと日本語入力

マルチバイト文字は上手く入力できません。入力値はascii文字に限定し、テスト端末のシステム言語を英語にするなどしてIMEの入力モードを英語に固定するのが無難です。

# テストの実行

UIテストを実行するエミュレータ or デバッグ接続した実機（iOSは実機不可）を用意した状態で、以下のコマンドを実行します。Terminal上に現在のステップが表示されるので最後まで実行を待てば完了です🎉

```bash
maestro test ${path_to_flow.yaml}
```

添付動画のアプリとFlowファイルは[GitHubで公開しています。](https://github.com/Seo-4d696b75/dart-async-test-sample/tree/maestro)

![maestro_sample_android.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/85200649-6a2f-bdd2-5369-7f9b9001e192.gif)


![maestro_sample_ios.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/80c3d13c-07e5-628d-e133-81d5b20f0eb5.gif)