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

1d1580fb0945b0ffadff18e28bead3c5?s=47 いも
June 06, 2018

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

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

1d1580fb0945b0ffadff18e28bead3c5?s=128

いも

June 06, 2018
Tweet

Transcript

  1. Unityプロダクトに テストを導入していくまで いも 2018/6/6 Unityテスト完全に理解した

  2. いもです いも(28) @adarapata テスト完全に理解してない方 GMOペパボ(2012/4~2018/2) php,rails,android ミクシィ XFLAG(2018/3~) Unity 趣味でUnity

    -> 仕事でUnity
  3. 会社でやってること 設計おじさん ふりかえりおじさん チームビルディングおじさん テストおじさん ←今日はこれ

  4. なぜテストおじさんになった のか

  5. チームの状態 プロダクトコードにテストはない状態 テストコード経験者はチーム内でほぼいない 「興味はあるが業務でやったことはない」状態 テストコードに否定的ではない

  6. 自身の経験 前職はテスト文化が根付いてたので書いていた phpunit, rspec, robolectric, tape APIなどが多めだったのでUIテストはあまり経験な し Unityは趣味でUnitテストのみ書いてた UIはほぼなし

  7. 外からの知見を使えそう ゲーム系とWEB系で技術領域・視点が違うように 感じた 他社・他業種から来たからこそやれることがあり そう 興味自体は持っていたので、環境が揃えばうまく 回ると考えた そもそも僕がテスト無いと心がしんどくなる

  8. テストおじさんになりました

  9. テストおじさんの目標 テストできる環境を作る テストを書く文化を作る

  10. テストできる環境を作る

  11. インゲームとアウトゲーム 社内で使われている用語(業界で一般的?) コアの体験に当たる部分がインゲーム 繰り返し遊んでもらう導線部分がアウトゲーム

  12. インゲーム アウトゲーム

  13. どこにテストを導入するか? インゲームは内容によって要件がかなり異なるの で一般化しづらい。テスタブルな設計になってい ない場合もあるので、難易度が高そうに見える。 アウトゲームのUIは割とネイティブアプリやWEB に近いものが多いので非ゲームのテストノウハウ をそのまま適用でき、難易度はそこまで高くはな い 今回はアウトゲームのみテストコードを導入する ことにした

  14. ざっくりと整理 Unit Test UI Test インゲーム やらない やらない アウトゲーム やる

    やる
  15. アウトゲームの状態 MV(R)Pアーキテクチャを先日導入した それまでは特に決まりごとはなかった Pure Classは割と多いのでユニットテストしやすそ う シングルトン多め

  16. テスト書くぞ!

  17. ユニットテストを書く 特にライブラリは使わず PureClassのテストを粛々と書く まずは新規に作るモデルのテストを書き始めた 順調にいけば1クラス30分もかからない 既存のテストが少し時間かかる

  18. シングルトン問題 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; } }
  19. [TestFixture] public class UserCharacterDataTest { [Test] public void GetMaxHpTest() {

    var chara = new UserCharacterData(id: 1, level: 10); Assert.AreEqual(100, chara.GetMaxHp()); } } MasterDataって必ず存在するの? 通信してたらテストでも通信走っちゃうの? レベルデザイン調整入ったらテストも落ちる? ↑を気にしないといけないテストなの?
  20. シングルトンはテストしにくい 実体に強く依存する モックに差し替えづらい isTestみたいなフラグは持ちたくない ゲームはシングルトンが生まれやすいイメージ マスタデータ、サウンドマネージャetc... UnityだとシングルトンなMonobehaviorも作りが ち SingletonMonobehaviourと呼ばれるやつ

  21. テストを書いていくためにはシングル トンを減らす方法を考えなければなら ない

  22. Zenjectでシングルトンを減らす UnityでDIを行うライブラリ https://github.com/modesttree/Zenject コンテナにシングルトンだったものを持たせれば 近い運用ができる 抽象化できればテスト用に差し替えることもでき る

  23. 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;
  24. [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()); } }
  25. Injectできるようになれば、テスト時はモックに差 し替える仕組みを作りやすい Injectできる = 結合が疎になっている 差し替えられれば本番のデータ変更の影響も受け ない 環境に依存せずテストが書ける 単純なモックならクラスを作らずともMoqで書ける var

    master = new MasterCharacter(); var mock = new Mock<IMasterDataManager>(); mock.Setup(m => m.GetMasterCharacterById(1)) .Returns(master); Container.BindInstance(mock.Object);
  26. シングルトンのInstaller public class SingletonInstaller : MonoInstaller { public override void

    InstallBindings() { Container.Bind<MasterDataManager>() .FromNew().AsCached(); } } このInstallerをどこに置くか? 必ず存在するので、ゲーム初期化時には必要 あらゆるところからアクセスできてほしい。 どこからでもバインドされていないといけない
  27. ProjectContextに持たせる https://github.com/modesttree/Zenject#global- bindings-using-project-context グローバルなContext 各SceneContextから、ProjectContextでBindされ たインスタンスを参照できる 該当するシーンにSceneContextを用意しておけ ば、勝手にバインドしてくれる シングルトンのような必ず存在してほしいインスタン スを保持させるには向いてそう

  28. ProjectContextの注意点 ゲーム開始時には必ず指定のオブジェクトがバインド されるので、差し替えるのが難しくなる PlayModeTestでも呼ばれるのでPlayMode用のオ ブジェクトに差し替えは難しい EditModeTest時は呼ばれないので問題ない テストだろうが本番だろうが必ず存在してほしいもの だけをバインドするとよさそう。 それ以外のシングルトンはSceneParentingを使うの が無難

  29. 実例っぽいコード 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の継承やめたい シングルトンをやめたい リソース取得部分を差し替えれるようにしたい 一気にやるとつらいので段階的に修正をがんばる
  30. 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); } } これでロジックのユニットテストは書けるようになる (リソース取得先問題はまだ解決してない)
  31. MasterDataManagerのシングルトンをやめたい Installerを書く MasterDataManager.Instance で参照しているところを インスタンスに書き換える public class SingletonInstaller : MonoInstaller

    { public override void InstallBindings() { Container.Bind<MasterDataStore>() .FromNew().AsCached(); } }
  32. リソース取得をモックできるようにしたい Load メソッドをインスタンスメソッドにする MasterDataLoader をインタフェースに変える public interface IMasterDataLoader { IReadOnlyList<T>

    Load(); } private class TestDataLoader : IMasterDataLoader { IReadOnlyList<T> IMasterDataLoader.Load<T>() { // ローカルのファイルからデータ引っ張ってくる } }
  33. 最終的な変更 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();
  34. テストコード [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); } }
  35. Zenjectですぐにシングルトン無くせる? それなりに時間かかること多い 参照しているところが多くて影響範囲凄かったり 呼び出し側にもInjectが必要になってきたり MonoBehavior継承しててUnitTest書けなかったり SceneContextの追加も必要な場合シーンも修正が 必要 一回の作業が大きくなりがちなのでチームの状況など を見てどこまでやるか判断すべき

  36. 平行運用期間を設けるのもあり 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; // こっちでも呼べる
  37. なんもわからん 非同期のテストは書けないっぽい //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にすると動くがテスト実行側が待てな いので必ずテストが通ってしまう
  38. なんもわからん テストコードにasync/awaitは使わない 非同期になりそうなのは通信系とか元々テストできな い部分なのでモックしてしまい、テスト時は全部同期 的に呼んでしまうことにする。 非同期な激重タスクが現れたらごめん。 NUnit自体はasyncサポートしているようなのでUnity 内のNUnitのバージョンが上がれば変わるかも? http://simoneb.github.io/blog/2013/01/19/async- support-in-nunit/

  39. ユニットテスト導入まとめ 新規は難しくない 極力ロジックはPureClassに持っていく シングルトンが障害になる Zenjectで脱シングルトンすると楽 影響範囲が大きい時は平行運用もあり async/awaitは使わない

  40. UIテストを書く 基本的にUIテストは費用対効果があまり高くない ※1 何をテストしたいのか明確にしてそれが手間に見合う かを判断したい

  41. 求める要件 ゲームの遷移を網羅することで、改修時の不具合 に気づけるようにしたい タップとか長押しとかユーザのインプットを簡単 に再現したい UIテスト経験者が少ないので、1テスト数行程度 で書けるくらい敷居を下げたい 上記を満たしてくれそうなテストライブラリが ないか調べてみる

  42. Unity-UITest

  43. Unity-UITest https://github.com/taphos/unity-uitest UnityでUIのテストを行えるライブラリ ボタンを押させたり、シーンロードしたり 実機でも走らせられる

  44. 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")); } } 基本的な欲しい機能は備わっている
  45. よさそう

  46. 即導入は厳しかった namespaceが切られていない Inject attributeを定義してるのでZenjectと被る 最低限だけなので、意外と対応してないイベント も多い ClickはあるけどDownとかUp単体はないとか forkして改修を加えたうえで使うことにした https://github.com/adarapata/unity-uitest どこかでPR出したい

  47. こんな感じで書けた 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>())); } }
  48. 費用対効果はよさそうか? 1テストケース書くのに数分程度 コード量少なく、やることも単純なので敷居は低 い 品質がめっちゃ担保されるほどではない テストケースが増えれば効果を発揮しそう 結論:続ける価値はありそう

  49. UIテスト導入まとめ Unity-UITestで結構楽にかける プロダクトに合わせて拡張は必要そう スワイプとかさせるのは大変かも 少ないと恩恵を受けづらいので数をこなそう

  50. Jenkinsで自動テスト(WIP) 継続的にテストを回していきたいので以下をやる リポジトリにPushされたらEditModeTest、 PlayModeTestを実行する Jenkins NUnit Pluginでレポートを生成 結果をSlackに通知 CI通らなかった場合マージできないようにする

  51. なんもわからん .NET4.6環境でEditTestをCLIから走らせるとハン グする Macのみ発生、Winは起きない 2017.3の最後のパッチで治ったらしいが、2017.4 のLTSだと普通に落ちた https://forum.unity.com/threads/stalled-builds- following-a-cleanup-mono-after-running-unit- tests-using-unity-2017-3f3.512707/ .NETを3.5に戻したら治ったけどそれは厳しい

    -testPlatform playmode でもEditTestケース走らせら れたので一旦これで対応
  52. なんもわからん CLIでPlayModeテスト走らせたら止まってること がある Jenkinsサーバの画面見に行くと何故か再開する 止まってるときはUnityコンソールに何かしらのエ ラーが出てる 画面を見る = アクティブになることで再開? 現在原因調査中...

  53. なんもわからん

  54. テストおじさんの目標 テストできる環境を作る テストを書く文化を作る

  55. テストを書く文化を作る

  56. 文化を作るには? 環境ができても自然とテストが増えることはない 一人でやり続けても辛くなる チームがやる意味・意義を理解する 書ける環境で終わらせない 把握して体験して理解する

  57. やっていきを表明した 前職でよく使われてた言葉 これをやりたい!何故やり たいかというのを言語化し てビジョンを共有する やる前に書く チーム外にも拡散 いいじゃん!と肯定して行動するのを「のってい き」と言う https://speakerdeck.com/kentaro/the-secret-of-

    leadership-and-followership
  58. やっていきに書いた内容(一部 修正済) https://gist.github.com/adarapata/ c017ebb755ebf4543fd596adfb61 5974

  59. どうだったか 目的や意義を書いたので、やっていることがブレ 始めてないか立ち返ることができた 部署内外で「テストの人」という認識を持っても らえた 認識されれば関連する話や相談が流れてくる より認識が強くなり、この人がいるならできるだ ろうという安心感を持たせて良いサイクルを生み 出す

  60. ペアプロでテストを書いた 隣でサポートしながら実際にテストを書いてもら った ユニットテスト、UIテストどちらも オールグリーンを体験してもらった オールレッドも体験してもらった テストだけのつもりが結局リファクタリングまで やってもらった

  61. どうだったか どのようにテストを書くか、どこが原因でテスト が書きづらいのかという視点から設計の問題点に 気づくことができた 結果、シングルトン減らしに繋がった その後は自分でどんどんテスト書いてくれてる 続けていってよさそう

  62. 文化の醸成には時間がかかる 無理やりに導入しない 導入のためにルールを作っても浸透しない 「なぜやるのか」の意識を揃える 体感を伴う行動の変化を促す(自己説得) 体感できない理由と向き合って解決方法を探す 時間がない:タスクの完了条件に加えるetc.. 書き方がわからない:ペアプロしてみるetc.. ゆっくりやっていきましょう

  63. 全体まとめ ユニットテストやるならZenject入れるのお勧め UIテストは費用対効果を意識する テストによって何を解決したいのかを意識する テストは文化、文化を作っていくことを意識する

  64. おわり

  65. 引用 ※1 Fundamentals of Testing https://developer.android.com/training/testing/fu ndamentals