Airtestでテンプレート画像の整理を試行錯誤した話

# はじめに
今年の3月ごろに突然Airtestを使って自動化テストを作成できないかと言われ、
プログラミング初心者なりにうまく使いこなせるよう試行錯誤してきました。

この記事ではAirtestの画像認識に必要なスクリプト上のテンプレート画像を
どのように整理したか、ということを共有させていただこうと思います。

# Airtestとは
Airtestは中国のNetEase社による画像認識のAI技術を利用した、
デスクトップおよびスマートフォン用アプリケーションの自動操作テストツールです。

アプリケーションからテンプレート画像を取り込み、
それを画像認識に利用して自動操作を行うことができます。

自動テストのスクリプトはPythonで記述します。

独自のIDEが使用でき、
アプリケーションから取り込んだ画像認識用のテンプレート画像が
スクリプト上にそのまま表示されるのが特徴です。

https://airtest.netease.com/

# Airtestを使い始めたころ
Airtestを使い始めたときは、必要なテンプレート画像も少なかったので、
取り込んだテンプレート画像の整理をしようという発想がありませんでした。
そのためスクリプト上にそのまま画像が並んでいる状態でした。 

以下の画像は「AndroidのGoogle Chromeで『AIQVE ONE株式会社』と検索する」という例です。
![スクリーンショット 2021-11-29 123724.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2286534/c2100a78-0572-3e27-b3d3-f318c77e8b55.png)

テンプレート画像をそのまま使いまわしていると、
1カ所のUIが変更された場合でも、スクリプト上の複数個所で画像を編集する必要が出てきます。

Airtestに慣れてきて条件分岐が複雑になったり、自動操作を関数にまとめたりすると、
編集すべき画像を見落とす危険性が大きくなってきました。

# テンプレート画像を変数に格納してみた
そこで画像を扱いやすくするために、変数に代入して使いまわすことにしました。
これなら画像を変数に代入している部分だけ変更すればよいので、UI変更時の手間が少なくなりました。

※以下から簡略化のため、スクリプト内のテンプレート画像は <画像名> で表記します。

```python
chrome_icon = <Google Chromeのアイコン画像>
google_logo = <Googleのロゴ画像>
search_word = <検索語句(Android版Chromeの検索バー)の画像>
search_result = <検索結果画面での弊社サイトのリンク画像>
aiqveone_logo = <AIQVE ONEのロゴ画像>

def launch_chrome():
    touch(chrome_icon)
    wait(google_logo)

def search(word):
    touch(search_word)
    sleep(1)
    text(word)
    wait(aiqveone_logo)

# ・・・以下略・・・
```
この方法もスクリプトが短く、使用する画像が少ないなら有効ではありましたが、
画像が多くなってくると変数の命名に困ったり、
スクリプトの画像を変数に格納する部分が縦長になって見づらくなったりと、
さらにもう一工夫が必要に感じました。

また自動操作部分に画像がなくなったことで、
どの画面を操作しているのか
スクリプト上から分かりづらくもなってしまいました。

# 辞書型でまとめてみた
というわけで、次に辞書型でテンプレート画像をまとめてみれば、
それらを解決できそうだと考えました。

画面や機能ごとに辞書型の変数を作成し、
キーをテンプレート画像の名前、
値をテンプレート画像として、複数の画像をまとめることにしました。

```python
chrome = {
    "アイコン": <Google Chromeのアイコン画像>,
    "ロゴ": <Googleのロゴ画像>,
    "検索バー": <検索語句(Android版Chromeの検索バー)の画像>,
    "検索結果": <検索結果画面での弊社サイトのリンク画像>,
}

aiqveone = {
    "ロゴ": <AIQVE ONEのロゴ画像>,
}

def launch_chrome():
    touch(chrome['アイコン'])
    wait(chrome['ロゴ'])

def search(word):
    touch(chrome['検索バー'])
    sleep(1)
    text(word)
    wait(chrome['検索結果'])

# ・・・以下略・・・
```

辞書名を画面や機能名にして、キーを日本語でつけることで、
どの画面や機能を触っているか多少わかりやすくなりました。

また辞書型ということで、for文でループ処理に使いやすくなりました。

他にもキーと値のペアは横にも並べられるため、
画像が多くなってスクリプトが縦長になることも抑えられました。

ただ、キーを日本語にしたことで、日本語の漢字変換という新たな面倒もでてきました。
例: 戻る <-> もどる、閉じる <-> とじる、など

辞書型でまとめたことで別の部分で面倒臭さが出てきたものの、
しばらくはこの方法をとっていました。

# 画面や機能ごとにクラスを作成するようにした
自動テストの作成に慣れてくると、
一つのスクリプトで数百もある一連のテストケースを回すようになり、
複数の画面や機能を行き来するようになりました。

それに伴って自動操作の関数がたくさん必要になり、
画像だけでなく関数も雑然としてきため、
スクリプトの全体像の把握が難しく、バグもすぐに直すことが困難になってきました。

そこで今度は画面や機能ごとにクラスを作り、
クラスごとに画像や自動操作のメソッドを管理することにチャレンジしました。

```python
class Chrome:
    temps = {
        "アイコン": <Google Chromeのアイコン画像>,
        "ロゴ": <Googleのロゴ画像>,
        "検索バー": <検索語句(Android版Chromeの検索バー)の画像>,
    }

    def __init__(self, word):
        self.word = word

    def launch(self):
        touch(self.temps['アイコン'])
        wait(self.temps['ロゴ'])

    def search(self):
        touch(self.temps['検索バー'])
        sleep(1)
        text(self.word)

class SearchResult:
    temps = {
        "検索結果": <検索結果画面での弊社サイトのリンク画像>,
        "ロゴ": <AIQVE ONEのロゴ画像>,
    }

    def __init__(self):
        pass

    def select_result(self):
        wait(self.temps['検索結果'])
        touch(self.temps['検索結果'])
        wait(self.temps['ロゴ'])

# ・・・以下略・・・
```

画面や機能ごとにクラスを分けることによって、
それぞれに適した自動操作のメソッドを作ることができ、
スクリプトを組みやすくなりました。

またテンプレート画像がなくても、
クラス名やインスタンス変数名からどの画面の操作をしているか、
辞書型を使用していたとき以上にわかりやすくなりました。

オブジェクト指向の考え方については、
Pythonの入門書等で何となくわかった気になっていましたが、
こうして実際に自分で工夫してみることでその便利さの一端を実感できました。

# まとめ
このようにプログラミング初心者ながら、
自動化スクリプトを効率的に作成できるよう試行錯誤しています。

とはいえ、どのようなスクリプトでもクラスを作らなければいけないということではなく、
スクリプトの長さによって上記の4パターンを使い分けるのが良いと思います。

まだまだ改善の余地はあると思いますので、
これからもテスト自動化に励んでいこうと思います。

最後まで読んでいただきありがとうございました。