Airtest+Pocoの自動化でのアプリとのデータの受け渡し方法

# はじめに

初めまして、QualiArtsでUnityエンジニアをしている たなひろ と申します。最近はAirtest+Pocoを使って[IDOLY PRIDE](https://idolypride.jp/)などの自動化に取り組んでいます。本記事では、自動化でPocoの操作のみでは対応できない状況で、RPCを使った自動化処理の方法について紹介します。

本記事は[QualiArts Advent Calendar 2022](https://qiita.com/advent-calendar/2022/qualiarts) 14日目の記事になります。

# Poco
自動テストを行う際、Pocoを利用してテストを行っている所は多いかと思います。PocoではUnityEditorのHierarchy上のObjectを指定して、Clickなどの操作をすることができます。しかし、その場合でも、時にはうまくテストができないようなことが起きて困ったことはないでしょうか。

例えば
・アプリを起動したときに、特定のサーバに接続してテストしたい
・今テストでどこまで進んだのかを判別したい
こんな時に、Pocoで使えるRPCを使って解決しています

# RPCについて
RPCとはRemote Procedure Call・リモートプロシージャコールの略で、ネットワーク越しに別のコンピュータ上のコードを呼び出すものです。今回はPocoが内部で使っているものを拡張して使用します。

以下、自動実行時の各ステップを呼んでいるAirtestの方をAirtest側と表記し、呼ばれているアプリ側をアプリ側と表記します。

## PocoのAirtest側実装
Pocoで使用している使っている場所はバラバラですが、例えば
```self.support_vr = self.client.call("isVrSupported")```
や
```return self.rpc.call("GetSDKVersion")```
というようなコードがあり、アプリ側からデータを取得するコードがあります

## Pocoのアプリ側実装
Pocoのアプリ側のマネージャーであるPocoManagerでは、以下のようになっています
```c#
public class PocoManager : MonoBehaviour
{
    // (省略)

    // ① RPCのAttributeの定義
    class RPC : Attribute
    {
    }

    void Awake()
    {
        // (省略)

        // ② RPCの登録
        rpc.addRpcMethod("isVRSupported", vr_support.isVRSupported);
        // (同様のaddRpcMethodが並んでいるので略)
        rpc.addRpcMethod("GetSDKVersion", GetSDKVersion);

        // (省略)
    }

    // (省略)

    // ③ 実際に呼ばれるMethod
    [RPC]
    private object GetSDKVersion(List<object> param)
    {
        return versionCode;
    }
}
```

① RPCのAttributeの定義を用意しています
```c#
class RPC : Attribute
{
}
```

② ここでPocoが使用しているRPCの登録をしています
RPCの呼び出し用文字列と、実際のMethodの紐付けをして、Methodの登録をしています
```c#
rpc.addRpcMethod("GetSDKVersion", GetSDKVersion);
```

③ 実際に呼ばれるMethodを用意します
[RPC]というAttributeをつけることと、引数は必ず「List<object> param」になります
```c#
[RPC]
private object GetSDKVersion(List<object> param)
{
    return versionCode;
}
```

このような実装でPocoでは、Airtest側からアプリ側のSDKVersionの取得を実装しています。
これを参考に、実装した例を紹介します。

# 実例 その１ アプリ側にデータを渡す
「アプリを起動したときに、特定のサーバに接続してテストしたい」という要件のために、Airtest側からアプリ側へサーバ番号を渡すような実装例になります。

## Airtest側からのサーバ番号送信
下記のようなコードで、SetServerというRPC呼び出しコードにintのサーバ番号を渡しています
```python
# UnityPocoの初期化
poco = UnityPoco()

# 開発サーバの指定をする
def set_dev_server(dev_number: int):
    return poco.agent.rpc.call("SetServer", dev_number).wait()
```

## アプリ側のサーバ番号受信
送信されたデータをこのようなMethodで受け取っています
```c#
[RPC]
private object SetServer(List<object> param)
{
    var devNo = Convert.ToInt32(param[0]);
    CommonOptions.Current.ApiDevNo = devNo;
    return True;
}
```
param[0]には、送信側のdev_numberが入りますのでintに変換して使用します

このようなコードで、Airtest側からアプリ側へデータを送っています

# 実例 その2 アプリ側からデータをもらう
「今テストでどこまで進んだのかを判別したい」という要件のために、アプリ側のenum値をAirtest側へ渡すような実装例になります。
アプリ内でチュートリアルの進行度をenum値で持っているのですが、その値をAirtest側で確認するために使っています。

## Airtest側からデータをもらうMethodの呼び出し
下記のようなコードで、SetServerというRPC呼び出しコードにintのサーバ番号を渡しています
```python
# UnityPocoの初期化
poco = UnityPoco()

# チュートリアルの進行状態を取得
def get_tutorial_state():
    (state, _) = poco.agent.rpc.call("GetTutorialState").wait()
    return state
```

## アプリ側のチュートリアルの進行状態を返す
```c#
[RPC]
private object GetTutorialState(List<object> param)
{
    return Tutorial.GetCurrentTutorialStep().ToString();
}
```

rpcのtupleの前側に、アプリで返したstringが入りますのでそれを確認しています

# PocoManagerの修正
先ほど、PocoManagerでRPCの登録処理を行っていましたが、上記の実例で書いたRPCコードも登録処理が必要です。
```c#
void Awake()
{
    // (省略)

    // ② RPCの登録
    rpc.addRpcMethod("isVRSupported", vr_support.isVRSupported);
    // (同様のaddRpcMethodが並んでいるので略)
    rpc.addRpcMethod("GetSDKVersion", GetSDKVersion);

    // ④ 追加
    rpc.addRpcMethod("SetServer", SetServer);
    rpc.addRpcMethod("GetTutorialState", GetTutorialState);

    // (省略)
}
```
④ 既存のPocoのRPC登録の下に、自分で作ったRPCコードを追加する必要があります

ちなみに実際に使う際には、直接PocoのPocoManagerを変更しないようにするため、partial classにしています

# まとめ
今回はAirtest+Poco環境でのRPCを利用したデータのやりとりの方法について解説しました。単純にPocoのみで対応できない場合が起きてもRPCを使うことで対応できるパターンが増えるかと思います。
また実際に、プロジェクトで実行している自動テストの全体像については、また別途記事を書ければと思います。