テスト推進奮闘記 Now In REALITY Tech #118
アバターシステムチームでUnityエンジニアをしているホンダです。
これまで約1年半ほど、Unityで開発したコードに対しての自動テストの導入やチームにテストを書く文化を浸透させるために試行錯誤してきました。本稿ではこれまで行なってきたことの振り返りをもとに所感をまとめようと思います。
あくまで「うちのチームではこう進めています」という内容なので、このやり方を参考にしてくださいというほどの説得力はありませんが、自分と同じように自動テストを導入したい!と奮闘している方の何かしらの助けになればと思います。
自動テストを導入する上での課題
自動テストを導入する前のプロジェクトやチームの課題は以下のような感じでした。
プロダクトコードがAssembly-CSharpに含まれておりテストのAssemblyからアクセスできずテストが実行できない
自動テストを実行できる環境がない
テストの書き方がわからない
コードのテスト容易性が低い
コードによって設計やパターン(MVP、MVVMなど)がバラバラ
この中でもコードのテスト容易性の低さや設計や用いられているパターンにばらつきがあることが特に対応が困難で、テスト容易性をあげるために大規模なリファクタを行おうにもテストがなく挙動を保証できるものがないので手が出しづらいというジレンマが発生します。また、テストを書くという目的を持っていても、より良いテストとは何かという文脈からより良い設計とは何かという話に膨れ上がり、設計の良し悪しを決めかねて結局何も進まないというようなことも起こりました。
これまでにやってきたこと
上記の課題を抱えた導入前の状態からこれまでに大きく時系列順に以下の6つのフェーズに分けて進めてきました。各フェーズごとにやってきたことをまとめていきます。
自動テストが動く環境の整備
チーム全体で最低限のテストの知識を身につける
実際にテストを書いてみる
現在のコードに対してテストを書く
テストを追加しやすい環境づくり
テストの質を高める
1.自動テストが動く環境の整備
とりあえずテストが実行できるように
とにかくテストを実行できる環境がないと話にならないということで、自動テストが実行できる環境を作ることから始めました。
まず、そもそもテストのAssemblyからプロダクトコードにアクセスできない状態だったため、プロダクトコードをまとめたAssembly Definitionを追加しました。この時もっと細かく定義して細かく分けるべきではという話も出ましたが、コードの依存関係的にもすんなり分けられないだろうということで見送りました。また、スクリプトのAssemblyが変わることでAssetBundleに互換性がなくなってしまうので、Unityのバージョンアップに合わせることで回避しました。
自動テスト環境の構築
次にGithub Actionsを使いself-hosted runner上で動作するプルリクエストベースの自動テスト機能を実装しました。詳細については過去に記事を書いているのでそちらを参考にしてください。
テストが通らなければマージできないようにブロックするところまでをこのフェーズのゴールとしました。
2.チーム全体で最低限のテストの知識を身につける
このフェーズでは輪読会を開き、チーム全体でテストに対する知識を深めました。この時使用した書籍はこちらです。
他にもいくつか書籍の候補はありましたが、自分たちの扱う環境により近いUnityを題材としたものが良いという判断でこちらの書籍が採用されました。
この中でNSubstituteというモックライブラリの使い方も併せて学びました。
3.実際にテストを書いてみる
モブプロを用いてテストを書く
前回のフェーズで知識をつけたので、いざ書いてみようということでモブプロ(※1)を採用しチーム全体のテストに対しての技量の底上げを行いました。モブプロ会は2hを週に2回のペースで行い、1週間ごとにゲストを交代しながら進めました。当時は自分自身も本などから学んでいる最中で、明確にこうすべきという決断が難しい時もありましたが、メンバーと相談しながら進められたのはかなり効果があったように思います。テストに対しての技術力が向上したのでモブプロおすすめです。
反省点
ただ、このモブプロ会を進めるにあたって一つ反省点もありました。というのもモブプロと言いつつも雰囲気モブプロという感じで厳密に進められておらず、ゴールを明確にできないまま進めていたことでした。テスト対象のクラスの複雑さによっては、その日の間もしくはその週にすら対応しきれず翌週にずれることも珍しくなく、続きから始まった場合に担当のゲストが変わることで経験値にばらつきが生まれてしまいました。
最終的に、どのメソッドにテストを書くか、というゴールを明確に決め、その日のうちにプルリクエストを作成してレビューしてもらうというルールを設けることで安定してコミットし続けられるようになりました。
4.現在のコードに対してテストを書く
テストを書くためのリファクタはすべきなのか
プロダクトコードの大半は設計時点でテストのことを考慮できていないものが多く、リファクタするにも課題であげたジレンマが発生します。なのであくまでテストを書くことを目的に大規模なリファクタは行わずに現在のコードに対してテストを追加することを意識して進めました。
コードのテスト追加難易度の可視化
テスト書くことに慣れるためにも比較的簡単なコードからテストを書いていく方針で進めました。その際どういうコードがテストを書く難易度が高いのかをある程度かしかできていた方が便利だと思い、スコア付けして一覧で確認できる機能を作りました。
スコアリングはざっくりですが以下の基準で行っています。それぞれどういうスコアづけをしているかまで書くとそれだけで1つ記事が書けてしまうので割愛します。
MonoBehaviourを継承しているかどうか
クラスが抱えるメソッドの総数
privateメソッドの比率
他クラスの依存数
コンストラクタの引数の数
SerializeFieldを持っているか
メソッドの戻り値のタイプ
特定の高難易度になりがちなクラスを引数、または戻り値として持っているか
高難易度になりがちな構造はブラックリスト形式でリストアップして判断しています
難易度が低いクラスからチーム全体で集中的にテストを書く
弊社には普段の施策開発では手が回らない範囲の改善に集中できる改善Weekという期間が四半期ごとに1週間あります。その改善Weekを活用してチームメンバー全員で難易度の低いクラスからテストを書き続けるという取り組みも行いました。この期間で100以上のクラスにテストを追加することができ、実際に手を動かすことで勉強会、モブプロで得た知識をさらに引き上げることができました。
テストを書く上での悩み
ここで悩んだのはどこまでモックすべきなのかという点でした。テスト容易性が低いままのコードをテストするには多くのクラスの依存解決や何をテストすべきかという観点で頭を悩ませることになります。このフェーズで行ったことは依存するクラスのinterfaceを片っ端から追加してモックすることでロジックのテストを行うというものでした。
これでテストを書くことはできましたが、今後テスト容易性を高めるような変更を行いたいときに壊れてしまう可能性が高いです。またモックする範囲が広いとテストに依存したロジックが生まれてしまうこともあります。モックの使用を最低限に留めることで回避できた可能性もありますが、それはそれでテストを書く難易度がかなり高くなるので一長一短に感じました。
5.テストを追加しやすい環境づくり
テストを書く範囲を明確に定義する
4のフェーズは自分で黙々と進めることもできた内容ですが、今後自然とテストが増えていく状態を目指すのであればメンバー全員がテストを追加して行ける状態になっていた方が望ましいです。そのためにも最低限この範囲は書いていきましょうという共通認識が必要になります。現時点ではMVPパターンでいうModel層にはテストを追加していこうという方針で進めています。コード上はModel層に値するコードは〇〇Serviceという命名になっているので、Serviceにはテストを追加しようということになります。
PresenterにもModelを組み合わせて扱うことでロジックが生まれる可能性はあるのでテストはあった方がいいかなという考えではいますが、PresenterのコードがMonoBehaviourを継承していてViewのGameObjectとセットになっている場合が多く、モックによる挙動の差し替えが難しいことが多いため一旦優先度は下げました。
テストを追加する上での心理的ハードル
メンバーがテストを書こうとした時の心理的ハードルについて考えてみました。自分がテストを書くときに手が止まりがちだなと思うのは以下のようなケースです。
テストファイルがそもそも存在しない
テストが通る状態にするまでが大変
テストの書き方の参考になるようなコードがない
テストファイルを追加するところからとなると、テストを最低限動かすために必要な依存解決などから始める必要が出てきます。それはクラスの複雑度にもよりますがかなり面倒に感じます。またテストケースを追加しようとした場合にも、慣れていない場合参考にできるコードがあった方が追加しやすいはずです。
それらを解決するために、すべてのModel層(ここでいうModel層は〇〇Serviceというクラス)のクラスにテストファイルとテストケースをいくつか追加し参考にできるテストケースが存在するという状態を目指しました。
6.テストの質を高める
カバレッジの計測
チーム内で共通で確認できる指標の1つとしてカバレッジの計測を始めました。質を高めるという意味ではカバレッジはあくまで参考程度にしか扱えませんが、どれくらいのコードがカバーされているのか、直近の対応で下がっていないのかはチーム全体で共通認識として持っていた方がいいように思います。
設計と向き合う
これはまだ現在進行形ではありますが、チーム全体の設計のレベルを上げることも必要に感じました。より良いテストを書くためにはどういう設計がいいのかをチーム内で共通認識として持つことが大切だと思うので、こちらも輪読会を開催するなどして取り組んでいけたらなと思います。
やってきたことを振り返ってみて
これまでやってきたことを振り返ってみて、特に以下の部分がもう少し上手くやれたかもなと感じた部分です。
チームメンバーにテストを書くメリットをうまく伝える
テストを書きやすい設計をチーム内の共通認識としてもつ
テストを書きやすくするための機能の拡充
チームメンバーにテストを書くメリットをうまく伝える
テストを書くことは工数がかかることも事実です。これまでテストを書かずに機能開発していた場合に必要な工数の倍以上かかることもあります。そこまでの工数を使ってやるメリットはなんなのかということがメンバーに伝わらないとテストを書けと言われても書く気にならないという状態になってしまいます。
今回対応してきたのは既存のコードに対してテストを追加していくことが大半で、メリットとしてはリグレッションテストとして機能することが挙げられますが、書いてすぐには実感しにくいものでした。
もしメンバーが新規画面の実装タイミングなどで新たに設計する必要が出てきたときに、テストも一緒に追加していけるようにサポートすることでテストの他のメリットを実感できるタイミングは作れたかなと思いました。
テストを書きやすい設計をチーム内の共通認識としてもつ
一旦大規模なリファクタはせずにテストを書くことを目的として進めるという方針をとってきましたが、新規機能の実装時にどういう設計が望ましいのかがチーム内で認識がずれることがあり、テストが書きやすい設計はどういう設計なのかについては早い段階でチーム内で共通認識を持っておくべきだったなと思いました。定期的にどういう設計がテストを書きやすくするのかを議論する場を設けることで、もう少し全体の意識は高められたかなと思います。
テストを書きやすくするための機能の拡充
Unityという環境の都合で書きにくい部分もあると思います。例えばSerializeFieldを使っているMonoBehaviourなクラスをテストしたいとなった時に、テスト実行のためにGameObjectを用意してコンポーネントを追加してというのも面倒です。なのでテスト向けの拡張機能としてリフレクションを使って外から値をセットできるようにしようなどといった、不便な部分を書きやすくする取り組みはもう少しやれたかなと思いました。
最後に
これまで自動テストを導入するにあたって本を読んだりブログを漁るなど、さまざまな情報源から知識を取り入れながら進めてきました。その中で感じたことは、まず第一にプロジェクトの段階が違うことが多い、次にUnityという環境の都合に合わせたものが少ないように感じました。自動テストを導入するにあたってプロジェクトによって方法はさまざまなのでこれが正解というものはありませんが、テストを書く文化が根付いていない運用段階のプロジェクトでどうやって自動テストを導入したのかという事例が多く残っていれば助かった自分がいたように感じたので今回このような形で記事にしてみました。まだ完全に導入に成功できたわけではないですが引き続きコードと向き合っていきたいと思います。