Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unityプロダクトにテストを導入していくまで

いも
June 06, 2018

 Unityプロダクトにテストを導入していくまで

6/6 Unityテスト完全に理解した勉強会の資料です

いも

June 06, 2018
Tweet

More Decks by いも

Other Decks in Programming

Transcript

  1. シングルトン問題 public class MasterDataManager { public static MasterDataManager Instance {

    get; } } public class UserCharacterData { private int characterId; private int level; // キャラの最大HPの取得 public int GetMaxHp(){ var masterData = MasterDataManager .Instance .GetMasterCharacterById(characterId); // マスタが持ってるHPにレベルで補正がかかる的なやつ return masterData.Hp * level; } }
  2. [TestFixture] public class UserCharacterDataTest { [Test] public void GetMaxHpTest() {

    var chara = new UserCharacterData(id: 1, level: 10); Assert.AreEqual(100, chara.GetMaxHp()); } } MasterDataって必ず存在するの? 通信してたらテストでも通信走っちゃうの? レベルデザイン調整入ったらテストも落ちる? ↑を気にしないといけないテストなの?
  3. Zenjectでシングルトンを差し替える public interface IMasterDataManager { MasterCharacter GetMasterCharacterById(int id); } public

    class MasterDataManager : IMasterDataManager { public MasterCharacter GetMasterCharacterById(int id) {} } public class UserCharacterData { [Inject] private IMasterManager manager; private int characterId; private int level; public int GetMaxHp() { var masterData = manager .GetMasterCharacterById(characterId); return masterData.Hp * level;
  4. [TestFixture] public class UserCharacterDataTest : ZenjectUnitTestFixture class MasterDataManagerMock : IMasterDataManager

    { public MasterCharacter GetMasterCharacterById(int id // いい感じに返す } } [Test] public void GetMaxHpTest() { Container.Bind<UserCharacterData>() .FromNew().AsCached().WithArguments(1, 10); Container.Bind<IMasterDataManager>() .To<MasterDataManagerMock>() .FromNew().AsSingle(); var chara = Container.Resolve<UserCharacterData>(); Assert.AreEqual(100, chara.GetMaxHp()); } }
  5. シングルトンのInstaller public class SingletonInstaller : MonoInstaller { public override void

    InstallBindings() { Container.Bind<MasterDataManager>() .FromNew().AsCached(); } } このInstallerをどこに置くか? 必ず存在するので、ゲーム初期化時には必要 あらゆるところからアクセスできてほしい。 どこからでもバインドされていないといけない
  6. 実例っぽいコード public class MasterDataManager : SingletonMonobehavior<MasterDataManager> { public static void

    Load() { resources = MasterDataLoader.Load<MasterCharacter>(); } public MasterCharacter GetMasterCharacterById(int id) { return resources.find(c => c.id == id); } } MonoBehaviourの継承やめたい シングルトンをやめたい リソース取得部分を差し替えれるようにしたい 一気にやるとつらいので段階的に修正をがんばる
  7. MonoBehaviourの継承やめたい おもむろに変えると広範囲に影響が出るので まずはロジックのクラスを作成し、MonoBehaviour が持つ形にする public class MasterDataManager : SingletonMonobehavior<MasterDataManager> {

    private MasterDataStore store; // ロジック public static void Load() { store.Load(); } public MasterCharacter GetMasterCharacterById(int id) { return store.GetMasterCharacterById(id); } } これでロジックのユニットテストは書けるようになる (リソース取得先問題はまだ解決してない)
  8. リソース取得をモックできるようにしたい Load メソッドをインスタンスメソッドにする MasterDataLoader をインタフェースに変える public interface IMasterDataLoader { IReadOnlyList<T>

    Load(); } private class TestDataLoader : IMasterDataLoader { IReadOnlyList<T> IMasterDataLoader.Load<T>() { // ローカルのファイルからデータ引っ張ってくる } }
  9. 最終的な変更 public class MasterDataStore { [Inject] private IMasterDataLoader loader; public

    void Load() { resources = loader.Load<MasterCharacter>(); } public MasterCharacter GetMasterCharacterById(int id) { return resources.find(c => c.id == id); } } public class SingletonInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<IMasterDataLoader>() .To<MasterDataLoader>() .FromNew().AsCached(); Container.Bind<MasterDataStore>() .FromNew().AsCached();
  10. テストコード [TestFixture] public class MasterDataStoreTest : ZenjectUnitTestFixture { [Test] public

    void GetMasterCharacterByIdTest() { Container.Bind<IMasterDataLoader>() .To<TestDataLoader>() .FromNew().AsCached(); Container.Bind<MasterDataStore>() .FromNew().AsCached(); var store = Container.Resolve<MasterDataStore>(); var chara = store.GetMasterCharacterById(1); Assert.AreEqual("Foo", chara.name); } }
  11. 平行運用期間を設けるのもあり SingletonMonobehaviourをバインドすれば、Inject できて .Instance でも呼べるので、平行運用は可能。 public class SingletonInstaller : MonoInstaller

    { public override void InstallBindings() { Container.Bind<FooManager>() .FromComponentInNewPrefab(fooPrefab) .AsCached(); // 速生成してインスタンスを用意しておく Container.Resolve<FooManager>(); } } FooManager.Instance.DoFoo(); // 従来の呼び方 [Inject] FooManager fooManager; // こっちでも呼べる
  12. なんもわからん 非同期のテストは書けないっぽい //Method has non-void return value, but no result

    is expected [Test] public async Task AsyncTest() { Debug.Log("before"); await Task.Delay(2000); Debug.Log("after"); Assert.True(true); } Task 付けるとエラーで実行できない 引数をvoidにすると動くがテスト実行側が待てな いので必ずテストが通ってしまう
  13. public class TestExample : UITest { [UnityTest] public IEnumerator Example()

    { // シーン読み込み待ち yield return LoadScene("TestableScene"); // 任意のオブジェクトを押したことにする yield return Press("Button"); // テキストの中身をチェックする yield return AssertLabel("FooText", "Pressed"); // 任意のシーンが存在するかチェック yield return Waitfor(new SceneLoaded("BarScene")); } } 基本的な欲しい機能は備わっている
  14. こんな感じで書けた public class FooSceneTest : UITestBase { [UnityTest] public IEnumerator

    Hogeボタンを押してBar画面に遷移する() { yield return StartOutGameScene(SceneName.Foo); yield return Down("Hoge"); yield return WaitFor( new OutGameSceneLoaded(SceneName.Bar)); } [UnityTest] public IEnumerator Fugaボタンを長押でポップアップが現れる() { yield return StartOutGameScene(SceneName.Foo); yield return LongPress("Fuga"); yield return WaitFor( WaitFor(new ObjectAppeared<FooPopup>())); } }