【Unity】【Addressable】Addressableの絡むテストを書く方法と方針の一例

UnityでAddressableの絡むテストを書く方法と方針の一例です。

Unity2019.4.0

やりたいこと

UnityでAddressableアセットシステムが絡むテストを書くケースとして、
テスト用のアセットにアドレスを設定してAddressablesで読み込む必要があるケースを考えます。
テスト外に影響を与えないように&テスト外の設定の影響を受けないようにテストを書く必要があります。

本記事ではこの方法について考えます。

なおグループやエントリ(アドレス)をスクリプトから操作する方法については以下にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

方針

Addressableの絡むテストを書く上で留意しなければならない点は二つあります。

一点目はテスト用アセットのアドレスの取り扱いです。
テスト用アセットにはアドレスを設定する必要がありますが、テスト外に影響を与えないようにするためにテスト後にはこのアドレスを削除する必要があります。
これについてはIPrebuildSetupとIPostBuildCleanupを使って確実に処理が行われるようにします。
さらにこのエントリを所属させるグループについてはテスト専用のものを作り、
万が一PostBuildCleanup()で例外が発生して削除処理が漏れたとしてもグループごと消せば問題ないようにしておきます。

二点目はPlayModeの取り扱いです。
例えばPlayModeがUse Existing Buildになっているとアセットバンドルをビルドしないとテストが通りません。
が、ビルドの処理はテスト外に影響を与えがちで、テスト前の状態に完璧に戻すことが難しくなります。
そのため今回はテスト前にPlayModeをUse Asset Databaseに変えてテスト後に戻すこととし、テストはエディタでのみ行うことにします。
Addressables.ResourceManager.RegisterDiagnosticCallbackなどを使いたい場合はSimulate Groupsでもいいと思います。
またもし実機でがっつりテストしたいようなケースがある場合にはもう少し頑張る必要があります(が、その場合はもう少しテストが簡単にできるように設計を見直すべきかも)。

テストを書く

さてそれではテストを書いていきます。

using System;
using System.Collections;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.TestTools;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.AddressableAssets.Build.DataBuilders;

#endif

public class ExampleTest : IPrebuildSetup, IPostBuildCleanup
{
#if UNITY_EDITOR
    private const string TestPrefabPath = "Assets/AddressableTest/TestPrefab.prefab";
    private string ActivePlayModeDataBuilderIndexKey => $"{GetType().Name}_ActivePlayModeDataBuilderIndex";
    private const string TestGroupName = "Test Group";
#endif
    private const string TestPrefabAddress = "TestPrefab";

    public void Setup()
    {
#if UNITY_EDITOR
        var settings = AddressableAssetSettingsDefaultObject.Settings;
        if (settings == null)
        {
            throw new Exception($"{nameof(AddressableAssetSettingsDefaultObject)} is not found.");
        }

        // 現在のPlayModeを保存しておく
        EditorPrefs.SetInt(ActivePlayModeDataBuilderIndexKey, settings.ActivePlayModeDataBuilderIndex);
        // FastMode(AssetDatabaseから読み込み)に変更
        var fastModeIndex = settings.DataBuilders.FindIndex(x => x is BuildScriptFastMode);
        settings.ActivePlayModeDataBuilderIndex = fastModeIndex;
        
        // テスト用Prefabを所属させるためのグループのテンプレートを適当に取得
        // BundledAssetGroupSchemaがあるグループであれば問題ないのでそれを取得する
        var groupTemplate = settings.GroupTemplateObjects
            .OfType<AddressableAssetGroupTemplate>()
            .First(x => x.HasSchema(typeof(BundledAssetGroupSchema)));
        
        // グループを作成
        var group = settings.CreateGroup(TestGroupName, false, false, false, null, groupTemplate.GetTypes());
        groupTemplate.ApplyToAddressableAssetGroup(group);
                
        // グループにテスト用アセットを所属させる
        var guid = AssetDatabase.AssetPathToGUID(TestPrefabPath);
        var entry = settings.CreateOrMoveEntry(guid, group);
        entry.address = TestPrefabAddress; // アドレスを設定(初期値はTestPrefabPathなので設定しなくてもいいけど)
        
        // 変更を保存
        AssetDatabase.SaveAssets();
#endif
    }
    
    public void Cleanup()
    {
#if UNITY_EDITOR
        var settings = AddressableAssetSettingsDefaultObject.Settings;
        
        // PlayModeを元に戻す
        var playModeDataBuilderIndex = EditorPrefs.GetInt(ActivePlayModeDataBuilderIndexKey);
        settings.ActivePlayModeDataBuilderIndex = playModeDataBuilderIndex;
        
        // エントリを削除
        var guid = AssetDatabase.AssetPathToGUID(TestPrefabPath);
        settings.RemoveAssetEntry(guid);
        
        // グループを削除
        var group = settings.FindGroup(TestGroupName);
        settings.RemoveGroup(group);
        
        // 変更を保存
        AssetDatabase.SaveAssets();
#endif
    }

    [UnityTest]
    [UnityPlatform(RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor)] // エディタだけでテスト
    public IEnumerator Test()
    {
        // テスト用のアドレスでアセットが読み込めることを確認
        var op = Addressables.LoadAssetAsync<GameObject>(TestPrefabAddress);
        yield return op;
        Assert.IsTrue(op.Status == AsyncOperationStatus.Succeeded);
    }
}

テスト前にPlayModeをAssetDatabaseから読み込むように変更し、テスト後に戻しています。

またテスト専用のグループを新規作成してそこにエントリを所属させています。
この専用グループはテスト後に削除しています。

テストではテスト用アドレスで読み込みが成功し、テスト用じゃないアドレスで失敗することをチェックしています。
このテストを実行すると正常に完了することが確認できます。

f:id:halya_11:20201007214837p:plain
テスト成功

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com