How to use Zenject.SceneTestFixture and example clean architecture on unity.
中野 洋輔Moff Inc.Zenject.SceneTestFixtureと向き合ったnaninuneno_y
View Slide
Zenject.SceneTestFixture• Dependency Injection(依存性注入)のためのライブラリ• 過去の勉強会でも取り上げられている• Unity Zenject完全に理解した依存性注入?疎結合というキーワードが出始めた時、C#ならばinterfaceを使うのが常套手段InterfaceBに依存したClassAにClassBのインスタンスを渡してやるのが依存性注入
Zenject.SceneTestFixture• ZenjectではTest-Runnerでの実行もサポートされており、• テストコード上で専用のBind()を行うことができる• その内、シーンテストを行うためのものZenject/WritingAutomatedTests.md詳しくは自分でGoogle翻訳かけた版
そもそも何がしたいんだっけ?• 画面毎(≒シーン毎)の挙動をテストしたい• このボタンを押すとxxxが消えるとか• 名前の入力とか• アプリケーションロジックをテストしたい• ログイン画面のテストでサーバにアクセスしない• ゲームコントローラが無くてもテストできる
そもそもinterfaceって?• オブジェクト指向をより理解するために実際に書いて解説する(Qiita)• クラスの継承がコーディングの上で何が便利か• Interfaceの抽象化のメリット/デメリット• Unity開発で使える設計の話+Zenjectの紹介(SlideShare)• SOLID原則について説明• Zenjectの導入もある• 書籍• Adaptive Code ~ C#実践開発手法 第2版 (マイクロソフト関連書)• Clean Architecture 達人に学ぶソフトウェアの構造と設計
Clean Architecture• 手掛かり無く抽象化を行うよりは、アーキテクチャに則るのが良い• アプリケーションのロジックをUseCasesより内側に閉じ込める• テストに場合に青の層の実装を入れ替えるCleanArchitectureでひとつ 『上』 のコードを目指す:概念編(Qiita)
LoginUseCase LoginPresenter LoginViewILoginPresenter ILoginViewAuthController FirebaseAuthIAuthControllerDummyAuthIAuthログイン画面を作る場合…• 普通に使うときはFirebaseAuthを使い、テストで実行する場合はDummyAuthを使うよう切り替えたい• 依存先をinterfaceにしているので、DI次第で可能!
シーン上では…public class LoginMain : MonoBehaviour {IUseCase authUseCase;[Inject]void ConstructUseCases(IAuthController authController,ILoginPresenter loginPresenter) {authUseCase = new AuthUseCase(loginPresenter,authController,this);}void Awake() {authUseCase.Begin();}}• InjectメソッドでUseCaseの材料を受け取り、UseCaseを生成• MonoBehaviour.Awakeで実行CleanArchitectureでひとつ 『上』 のコードを目指す:実装編(Qiita)
シーン上では…public class LoginSceneInstaller : MonoInstaller {[SerializeField] LoginView loginView = default;public override void InstallBindings() {Container.Bind().FromInstance(new AuthController(FirebaseAuth.DefaultInstance)).AsCached();Container.Bind().FromInstance(new LoginPresenter(loginView)).AsCached();}}• [SerializeField]など実装の詳細はDIを行うこいつ集約
public class LoginSceneTest : SceneTestFixture {LoginPresenter loginPresenter = new LoginPresenter();AlertView alertView;LoginView loginView;AuthController auth = new AuthController(new DummyAuth());[UnityTest]public IEnumerator ログイン失敗でアラートが表示されるか() {// StaticContextにBindStaticContext.Container.Bind().FromInstance(loginPresenter).AsTransient();StaticContext.Container.Bind().FromInstance(auth).AsTransient();// シーン読み込みyield return LoadScene("Login");// シーンから欲しい要素をfindvar canvas = GameObject.Find("Canvas").transform;alertView = canvas.Find("AlertView").GetComponent();loginView = canvas.Find("LoginView").GetComponent();loginPresenter.View = loginView;// 適当なID/パスワード入力loginView.IdInputField.onEndEdit.Invoke("hoge");loginView.PasswordInputField.onEndEdit.Invoke("fuga");// 実行loginView.LoginButton.onClick.Invoke();yield return null;// アラートが表示されているかAssert.IsTrue(alertView.gameObject.activeSelf);// 閉じるボタンalertView.CloseButton.onClick.Invoke();yield return null;// アラートが消えたかAssert.IsFalse(alertView.gameObject.activeSelf);}}• なんかいけそうやん!
• StaticContextよりもSceneContextが優先されることを知り、InstallerのBindすべてに.IfNotBound()をつけた• LoadScene時にAwakeが実行されないようにMainをアタッチしたGameObjectをテストのときには一時非アクティブにするようにした• GitHub• 労力とリターンがあってない気がする(他にいい方法あるだろ)しかしこの後…naninunenoy/UnityViewPatterns/ViewPattern/BMIApp