コンテンツへスキップ

QualiArtsengineer blog

Anjinを利用したチュートリアル自動テストTips

Anjinを利用したチュートリアル自動テストTips

7 min read

はじめに

株式会社QualiArtsでUnityエンジニアをしている田中です。開発推進室にて自動テストを主な業務としています。 以前Airtestを使ったチュートリアル自動テストの実装を紹介しましたが、今回はAnjinを使ったチュートリアル自動テストを実装しました。 その際にいくつか引っかかったところがあったので紹介します。

チュートリアル自動テスト実装の目的

最初にチュートリアルを自動テストにて実装する目的について話します。 チュートリアルとは、初めてプレイするユーザーに向けてゲームの一連の流れを説明しながら体験してもらう機能です。 ゲームに必要な要素をユーザーに案内する重要な要素ですが、同時に複数の機能に関わるがゆえに壊れやすい要素でもあります。 チュートリアルが壊れているかどうかを検出するにはある程度の回帰テストが有効ですが、これを毎回手動でテストするのにもコストがかかります。 そのため自動でテストを実行することによって、チュートリアルが壊れているかを定期的に確認することができています。

Anjin導入の経緯

元々弊社ではAirtestを用いて自動テストを進めていました。 Airtestは、NetEase社が提供しているUI自動テストソリューションです。 UI自動テストソリューションとは、手動で行うユーザーインターフェース(UI)操作をスクリプトに変えて、自動で実行するためのツールやフレームワークを指します。 またAirtestでは実行環境として実機が必要になりますが、そちらはSGE Test Farm(以下STF)に用意されたAndroid実機に接続してテストを行っています。

現在でもIDORY PRIDEでは毎朝Airtestでチュートリアルを実行していますが、Airtestは内部でSocket通信をしているために通信の切断が多く発生します。 弊社ではJenkinsにてAirtestを動作させ、STFのAndroid実機へ接続していますが、その間のネットワークの状態などでSocket切断が発生します。 通信切断時に再接続などの処理は行っていますが、通常の就業時間などの通信切断される可能性のある時間を避けてテストを走らせるような状態でした。

そのころAnjinというUnityEditor上で動作するオートパイロットツールを知りそちらを導入してみることにしました。 Anjinは、株式会社ディー・エヌ・エーが開発・OSSとして公開しているUnity向けオートパイロットフレームワークです。詳しくはこちらのリンクをご参照ください:

オートパイロットフレームワークとは、UI操作をスクリプト上で自動的に実行するものです。

チュートリアル実装時の同一Sceneでのテスト不具合

Anjinは、アプリ側のSceneに対して、Sceneの中のUIをテストするためのTestAgentというコードがペアになるように用意する必要があります。 例えば、SceneAというものに対して、TestAgentAを作成しておきます。 オートパイロットを実行しSceneAが呼ばれるとそのSceneAをテストするTestAgentAが呼ばれ、そこに記述されたコードに従って処理が実行されます。このように1対1のセットでのテストが行われます。

1対1でのテスト

チュートリアルでAnjinを使用する際の工夫

今回テストしようとしているチュートリアルでは、「タイトル」や「ユーザー名の入力」、「キャラクターの選択」などの複数のSceneを行き来するようになっています。 その遷移の流れの中で同じSceneに戻って来るようになっている部分があります。 このとき実際には同じSceneAではあっても、チュートリアルの進行状態は違っていて、例えば1回目は「ユーザー名の入力」2回目は「キャラクターの選択」のように、同じSceneでも自動テストで処理すべき内容が違います。 しかし何もしないと、どちらの場合でも同じTestAgentAが実行されてしまうため、うまくテストをすることができません。

同一Sceneでのテスト1

そこで同一のSceneでも、適切なテスト処理をうまく実行するために、TestAgentAの先頭で分岐処理を入れるようにしました。 また分岐処理の条件にはチュートリアルの進行状態を利用することにしました。

public class TestAgentA
{
    protected override async UniTask OnRunAsync(CancellationToken token)
    {
        // チュートリアルの進行状態
        var tutorialStep = GetTutorialStep();

        switch (tutorialStep)
        {
            // ユーザー名の入力
            case TutorialStep.InputUserName:
                TutorialUserNameAgent();
                break;
            // キャラクターの選択
            case TutorialStep.CharacterSelect:
                TutorialCharacterSelectAgent();
                break;
        }
    }
}

同一Sceneでのテスト解決パターン

これにより同一のSceneであっても、チュートリアルの状態によって正常にテストが実行されるようになりました。

Click処理の実装

元々Airtestでチュートリアルを実装していたことから、Anjinでチュートリアルを実装する際にも、「Aのボタンを押す、次にBのボタンを押す」というように、それぞれのボタンを指定していくような処理を実装しました。

// 次へ
await ClickObjectAsync<MainButton>("SelectButton", token);
// アニメーション待ち
await WaitSecondsAsync(4f, token);
// TAP 押す
await ClickButtonAsync("OverlayButton", token);

実際のボタンを押す処理はExecuteEventsで実装してあります。

var eventData = new PointerEventData(EventSystem.current)
{
    // 押そうとしているpositionを画面座標に変換
    position = RectTransformUtility.WorldToScreenPoint(UICamera, target.position)
};

// ボタンを押す処理
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerDownHandler);
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerClickHandler);
await UniTask.Delay(TimeSpan.FromSeconds(0.2f), cancellationToken: ct);
if (target != null && target.gameObject != null) ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerUpHandler);

しかし、これによりいくつかの問題が発生しました。

Click処理時のGameObjectの指定ミス

ExecuteEventsでClickを行うときには、ClickするGameObjectを指定する必要があり、そのGameObjectはIEventSystemHandlerを持っていなければいけません。 しかし押そうとしているGameObjectが、ボタンのテキストなどを含む複雑な構造のPrefabになっているときに、IEventSystemHandlerを持っていないGameObjectを間違って指定してしまうことがあります。 その時でも特にエラーなどが出ることもないため、押せていないコードになっていることがありました。 そこで下記のようなコードを追加して、Click可能かチェックするようにしています。

bool ExistsPointerClickHandler(GameObject go)
{
    return go.GetComponent<IPointerClickHandler>() != null;
}

ここではIEventSystemHandlerを継承しているもののうち、クリックだけをチェックしているため、IPointerClickHandlerだけを見ています。 他にも、

  • public interface IPointerDownHandler : IEventSystemHandler
  • public interface IPointerUpHandler : IEventSystemHandler
  • public interface IDragHandler : IEventSystemHandler

など長押し(DownとUp)に使う物やDragに使う物などがありますので、必要に応じてチェックするとよいでしょう。

押せないはずのボタンを押すことができてしまう問題

上記のように押すボタンを直接指定しているため、同じ座標上にそのボタンを押せないように別の物をかぶせている場合でも、その裏側にあるGameObjectを押すことができてしまう作りになってしまっていました。 よくあるのが、チュートリアルではなく実際のゲーム状態で使うボタンが置いてあり、その上にチュートリアル専用のボタンを重ねていることがあります。 この時に間違って、チュートリアル用ではないボタンを押すようにコードを書いてしまっていた場合、そのまま押すことができてしまいます。

そのため、下記のようにEventSystem.current.RaycastAllを利用して、指定GameObjectの画面座標について、上から押すような実装に修正しました。

var eventData = new PointerEventData(EventSystem.current)
{
    // 押そうとしているpositionを画面座標に変換
    position = RectTransformUtility.WorldToScreenPoint(UICamera, target.position)
};

// eventDataのposition から、押そうとしている画面座標のRaycastを取得
var raycastResults = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);
// 一番上のRaycastを登録
eventData.pointerCurrentRaycast = raycastResults.FirstOrDefault(x => x.gameObject == target.gameObject);

// ボタンを押す処理
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerDownHandler);
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerClickHandler);
await UniTask.Delay(TimeSpan.FromSeconds(0.2f), cancellationToken: ct);
if (target != null && target.gameObject != null) ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerUpHandler);

細かく押すボタンを指定することによる問題

先ほどボタンを指定して順番に押していくように実装してあると書きましたが、これは少し問題がある状態です。 もしチュートリアル内の流れに変更が入った場合に、ボタンの指定などを毎回修正しなくてはなりません。 そのためボタン指定するやり方ではなく、その時押せるボタンをランダムに押しつつ先に進むやり方(モンキーテスト)の方法に変更することを予定しています。 今回のチュートリアルは、ランダムにボタンを押すやり方でも(多少同じ所を行き来することもありますが)基本的にボタンを押すことで次へ進めるようになっているため、モンキーテストで実行しても問題ありません。 モンキーテストにすることで、ソフトウェアの通常の操作では見逃されがちなバグを発見できるようになります。 実際にモンキーテストに変更する話は次の機会に説明しようかと思います。

おわりに

Unity製モバイルゲームにおけるAnjinを利用した自動テストで、実装したときにはまった点についていくつか紹介しました。 QualiArtsではUI自動テストに限らず、他にもいろいろなテストについてのチャレンジを予定しています。 自動テストを行う方々の参考になれば幸いです。

2010年に株式会社サイバーエージェントにサーバーサイドエンジニアとして中途入社。その後、QualiArtsにてUnityエンジニアとして開発に携わる