Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

テストできる環境を作る

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

インゲーム アウトゲーム

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

テスト書くぞ!

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

シングルトン問題 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; } }

Slide 19

Slide 19 text

[TestFixture] public class UserCharacterDataTest { [Test] public void GetMaxHpTest() { var chara = new UserCharacterData(id: 1, level: 10); Assert.AreEqual(100, chara.GetMaxHp()); } } MasterDataって必ず存在するの? 通信してたらテストでも通信走っちゃうの? レベルデザイン調整入ったらテストも落ちる? ↑を気にしないといけないテストなの?

Slide 20

Slide 20 text

シングルトンはテストしにくい 実体に強く依存する モックに差し替えづらい isTestみたいなフラグは持ちたくない ゲームはシングルトンが生まれやすいイメージ マスタデータ、サウンドマネージャetc... UnityだとシングルトンなMonobehaviorも作りが ち SingletonMonobehaviourと呼ばれるやつ

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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;

Slide 24

Slide 24 text

[TestFixture] public class UserCharacterDataTest : ZenjectUnitTestFixture class MasterDataManagerMock : IMasterDataManager { public MasterCharacter GetMasterCharacterById(int id // いい感じに返す } } [Test] public void GetMaxHpTest() { Container.Bind() .FromNew().AsCached().WithArguments(1, 10); Container.Bind() .To() .FromNew().AsSingle(); var chara = Container.Resolve(); Assert.AreEqual(100, chara.GetMaxHp()); } }

Slide 25

Slide 25 text

Injectできるようになれば、テスト時はモックに差 し替える仕組みを作りやすい Injectできる = 結合が疎になっている 差し替えられれば本番のデータ変更の影響も受け ない 環境に依存せずテストが書ける 単純なモックならクラスを作らずともMoqで書ける var master = new MasterCharacter(); var mock = new Mock(); mock.Setup(m => m.GetMasterCharacterById(1)) .Returns(master); Container.BindInstance(mock.Object);

Slide 26

Slide 26 text

シングルトンのInstaller public class SingletonInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind() .FromNew().AsCached(); } } このInstallerをどこに置くか? 必ず存在するので、ゲーム初期化時には必要 あらゆるところからアクセスできてほしい。 どこからでもバインドされていないといけない

Slide 27

Slide 27 text

ProjectContextに持たせる https://github.com/modesttree/Zenject#global- bindings-using-project-context グローバルなContext 各SceneContextから、ProjectContextでBindされ たインスタンスを参照できる 該当するシーンにSceneContextを用意しておけ ば、勝手にバインドしてくれる シングルトンのような必ず存在してほしいインスタン スを保持させるには向いてそう

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

実例っぽいコード public class MasterDataManager : SingletonMonobehavior { public static void Load() { resources = MasterDataLoader.Load(); } public MasterCharacter GetMasterCharacterById(int id) { return resources.find(c => c.id == id); } } MonoBehaviourの継承やめたい シングルトンをやめたい リソース取得部分を差し替えれるようにしたい 一気にやるとつらいので段階的に修正をがんばる

Slide 30

Slide 30 text

MonoBehaviourの継承やめたい おもむろに変えると広範囲に影響が出るので まずはロジックのクラスを作成し、MonoBehaviour が持つ形にする public class MasterDataManager : SingletonMonobehavior { private MasterDataStore store; // ロジック public static void Load() { store.Load(); } public MasterCharacter GetMasterCharacterById(int id) { return store.GetMasterCharacterById(id); } } これでロジックのユニットテストは書けるようになる (リソース取得先問題はまだ解決してない)

Slide 31

Slide 31 text

MasterDataManagerのシングルトンをやめたい Installerを書く MasterDataManager.Instance で参照しているところを インスタンスに書き換える public class SingletonInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind() .FromNew().AsCached(); } }

Slide 32

Slide 32 text

リソース取得をモックできるようにしたい Load メソッドをインスタンスメソッドにする MasterDataLoader をインタフェースに変える public interface IMasterDataLoader { IReadOnlyList Load(); } private class TestDataLoader : IMasterDataLoader { IReadOnlyList IMasterDataLoader.Load() { // ローカルのファイルからデータ引っ張ってくる } }

Slide 33

Slide 33 text

最終的な変更 public class MasterDataStore { [Inject] private IMasterDataLoader loader; public void Load() { resources = loader.Load(); } public MasterCharacter GetMasterCharacterById(int id) { return resources.find(c => c.id == id); } } public class SingletonInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind() .To() .FromNew().AsCached(); Container.Bind() .FromNew().AsCached();

Slide 34

Slide 34 text

テストコード [TestFixture] public class MasterDataStoreTest : ZenjectUnitTestFixture { [Test] public void GetMasterCharacterByIdTest() { Container.Bind() .To() .FromNew().AsCached(); Container.Bind() .FromNew().AsCached(); var store = Container.Resolve(); var chara = store.GetMasterCharacterById(1); Assert.AreEqual("Foo", chara.name); } }

Slide 35

Slide 35 text

Zenjectですぐにシングルトン無くせる? それなりに時間かかること多い 参照しているところが多くて影響範囲凄かったり 呼び出し側にもInjectが必要になってきたり MonoBehavior継承しててUnitTest書けなかったり SceneContextの追加も必要な場合シーンも修正が 必要 一回の作業が大きくなりがちなのでチームの状況など を見てどこまでやるか判断すべき

Slide 36

Slide 36 text

平行運用期間を設けるのもあり SingletonMonobehaviourをバインドすれば、Inject できて .Instance でも呼べるので、平行運用は可能。 public class SingletonInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind() .FromComponentInNewPrefab(fooPrefab) .AsCached(); // 速生成してインスタンスを用意しておく Container.Resolve(); } } FooManager.Instance.DoFoo(); // 従来の呼び方 [Inject] FooManager fooManager; // こっちでも呼べる

Slide 37

Slide 37 text

なんもわからん 非同期のテストは書けないっぽい //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にすると動くがテスト実行側が待てな いので必ずテストが通ってしまう

Slide 38

Slide 38 text

なんもわからん テストコードにasync/awaitは使わない 非同期になりそうなのは通信系とか元々テストできな い部分なのでモックしてしまい、テスト時は全部同期 的に呼んでしまうことにする。 非同期な激重タスクが現れたらごめん。 NUnit自体はasyncサポートしているようなのでUnity 内のNUnitのバージョンが上がれば変わるかも? http://simoneb.github.io/blog/2013/01/19/async- support-in-nunit/

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Unity-UITest

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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")); } } 基本的な欲しい機能は備わっている

Slide 45

Slide 45 text

よさそう

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

こんな感じで書けた 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())); } }

Slide 48

Slide 48 text

費用対効果はよさそうか? 1テストケース書くのに数分程度 コード量少なく、やることも単純なので敷居は低 い 品質がめっちゃ担保されるほどではない テストケースが増えれば効果を発揮しそう 結論:続ける価値はありそう

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

なんもわからん .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ケース走らせら れたので一旦これで対応

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

なんもわからん

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

テストを書く文化を作る

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

おわり

Slide 65

Slide 65 text

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