※ 本記事にはプロモーションが含まれています。
こんにちは! ねこです。
今回は、XFLAG Tech Noteに学ぶ、Unity テスト駆動開発。
ということで、XFLAG Tech Note の MVP アーキテクチャにのっとって、テスト駆動開発を行ってみましたので、その有用性を書いていきます。
Unity で MVP 設計を初めてやった人が、ついでにテストをやってみたら、どうなったのか?
それでは、ご覧ください。
ぜひ、前の記事もお読みください
まずは、こちらの記事を先に読んでいただくと、ご理解いただきやすいです。
[sitecard subtitle=関連記事 url=/unity-mvp-with-xflag-tech-note/ target=]
現在、「学ぶは、まねぶ」の精神のもと、XFLAG Tech Note に書かれてある内容を、可能な限り再現しています。
今回の記事は、その続きとして、テスト駆動開発について書いていきたいと思います。
XFLAG Tech Note とは?
モンストの開発・運用元で有名な XFLAG さんが、技術書だけの同人イベント「技術辞典5」で出展していたものです。
現在は、無料で公開されており、誰でも読むことができるようになっています。
今回も、その中の、第 3 章「とある Unity 開発事例」を参考に書かせていただきます。
テスト駆動開発を行おう
テスト駆動開発をするにあたって、やはり、内容をある程度理解しておく必要があります。
テスト駆動開発とは
テスト駆動開発をざっくりと説明しますと、プログラム開発手法の一種です。
進め方を簡単に説明します。
動くプログラムを書く前に、先に要件をテスト&チェックするコードを書きます。
もちろん、そのままだと、テスト結果は、失敗(レッド)になります。
これを、いったんテストがとおるコード(グリーン)を書き、エラーをなくします。
そして、後日ちゃんと作り込み(リファクタリング)を行うのです。
もし、テスト駆動開発の詳細を知りたい方は、こちらの記事が参考になります。
記事みた中で一番、平文で読みやすく、芯をくっているような気がします。
ところで、なぜ、テスト駆動開発をする必要があるんでしょうか?
テスト駆動開発のメリット
テスト駆動開発のメリットを表すと、こんな感じだと思います。
- ソフトウエアの品質を一定に担保できる
- 開発後期の開発スピードの低下の軽減
- 開発者の心理的負担の軽減
私が一番いいと思うのが、開発者の心理的な負担の軽減です。
少なくとも、レビュアーは、テストに書かれている要件については、全て通っているという前提で、レビューできます。
また、開発後期となると、不安になる修正の影響範囲とエンバグ問題。
動くけど、実装がめちゃくちゃなコードは、場合によっては、たった 1 行を変えるだけでも、緊張が走ります。
場合によっては、それだけで結構デバック工数も発生しますよね。
しかし、テストを書いていくことで、そこに書かれた機能要件は満たしていることになり、余計なチェックや心理的な負担も減ります。
また、テストがし辛いクラスや、テスト工数が多すぎるクラスは、設計上、何かしらの問題があります。
そこも、見える化されそうなのも、副次的なメリットかもしれません。
これは、コードを書く側も、見る側も幸せになれそうです。
Unity 開発でテストはいらない?
はい。私もそう思ってました。実際にテストを書くまでは。
私自身、実際に運用にはのせているわけではないので、今後進めるのであれば、様々な問題に対処する必要は出てくると思います。
しかし、それを差し置いても、テスト駆動開発の有用性は、ひしひしと感じました。
ちゃんと稼働すれば、チーム開発を行う上での、エンジニアの心理的な負担が、だいぶん減りそうです。
ビヘイビア(振る舞い)駆動開発と言うべき?
今まで、テスト駆動開発やったことなかった私です。
ですので、「テスト駆動開発」という言葉に、ぴんと来なかったんですよね。
でも、テスト駆動開発 ≒ ビヘイビア(振る舞い)駆動開発と聞いて、納得しました。
簡単に言うと、テストコードに書く内容に、要件にのっとった振る舞いを書く。ということです。
それなら、先に書いておくメリットがわかりやすく伝わります。
例えば、「先にテストを書く」、というより、「先に振る舞いを書く」。と、言った方がしっくり来ますよね。
実際、要件(振る舞い)を先に決めてしまう手法ですので、ビヘイビア(振る舞い)駆動開発というべきかもしれません。
依存性の注入(Dependency injection)
テスト稼働開発をするにあたって、疎結合を維持するために必要とされるテクニックである、依存性の注入。
こちらの説明をしていこうと思います。
ちなみに、XFLAG Tech Note で、Zenject の記載がありましたが、今回は説明しません。
Zenject とは依存性の注入を行いやすくするためのフレームワークです。
依存性の注入 = Zenject ではありませんので、ご注意下さい。
内容は、下で説明したものを、より簡単に行えるものだと思って頂ければと思います。
ずっと疑問だった、この書き方
XFLAG Tech Note の MVP アーキテクチャを、マネするにあたって、メリットのわからなかったこの書き方。
public class SampleRepository : ISampleRepository
{
private SampleEntity entity;
public SampleRepository (ILoader loader) // ← ここの部分
{
entity = loader.Load<SampleEntity>();
}
}
public class SamplePresenter : MonoBehaviour
{
public void Initialize (IApiClient client, ISampleRepository repository) // ← ここの部分
{
}
}
なぜ、わざわざ、外側から必要データを渡す必要があるのか?
元の記事では、なぜ、そのようにしているのかが、はっきりと書かれて無かったため、意図を汲み取れてませんでした。
しかし、これこそが、テスト駆動開発に重要な役割をしめしているのです。
依存性の注入(Dependency injection : DI)とは
上記にあるような、クラスに必要なパーツ(クラス)を外部から取得するテクニック。
それを、依存性の注入(Dependency injection、略して DI )といいます。
依存性を注入するという、非合法の香りただようネームですが、れっきとしたソフトウエアの設計(デザイン)パターンです。
ちなみに、Dependency = 依存性 という名前ですが、もとの意味としては、「オブジェクト」に近いです。
ですので、じっさいには「依存オブジェクトの注入」というほうが、より内容を表していると思います。
意味としては、そのクラスの成り立ちに必要で、依存しているオブジェクトを、外部から注入するソフトウエアパターンのことです。
依存性の注入(DI)を行うと、以下のメリットがあります。
- 結合度の低下によるコンポーネント化の促進
- 単体テストの効率化
- 特定のフレームワークへの依存度低下
要するに、オブジェクト同士のつながりが薄く(疎結合に)なるから、パーツ単位で取り出しやすいし、変化にも対応しやすくなるよね。ということです。
テストを書いたら、書きやすい
なぜ、依存性の注入(DI)を行うのか?
それは、テストコードを書いて分かりました。
すごく、テストが書きやすいのです。
例えば、このクラスの GetName ()をテストしたいと思います。
// SampleRepository.cs ※テストしたいクラス
public class SampleRepository
{
private SampleEntity entity;
public SampleRepository (ILoader loader)
{
entity = loader.Load<SampleEntity>();
}
public string GetName ()
{
return "名前は、" + entity.name + "です。";
}
}
上記のコードを、テストするサンプルコードがこちらです。
// TestLoader.cs ※デバッグ用 Loader クラス
public class TestLoader : LoaderBase, ILoader
{
public TestLoader ()
{
SampleEntity sampleEntity = new SampleEntity();
sampleEntity.name = "サンプル名";
// オブジェクトを追加
base.AddObject(sampleEntity);
}
}
// SampleRepositoryTest.cs ※SampleRepository を、テストするクラス
public class SampleRepositoryTest
{
SampleRepository repository;
[SetUp]
public void InitializeAllTest ()
{
var loader = new TestLoader ();
repository = new SampleRepository (loader);
}
[Test]
public void 名前チェック ()
{
Assert.AreEqual (repository.GetName (), "名前は、サンプル名です。");
}
}
初期化も簡単、テストも簡単なのが、理解できると思います。
しかし、例えば、このクラスをテストしようと思ったらどうでしょうか?
// SampleBadRepository.cs ※テストしたくても出来ないクラス
public class SampleBadRepository
{
private SampleBadEntity entity;
public SampleBadRepository ()
{
entity = new SampleBadEntity ();
entity.name = "サンプル名" + Random.Range (0, 100).ToString ();
}
public string GetName ()
{
return "名前は、" + entity.name + "です。";
}
}
この、GetName () をテストする場合、受け取るのは、中で隠された文字列になってしまいますので、特定の文字で比較ができません。
Assert.AreEqual (repository.GetName (), /*ここに比較する文字を入れられない*/);
ここから、XFLAG Tech Note に示された書き方だと、なぜテストが書きやすくなったのか、説明していきたいと思います。
なぜテストが書きやすかったのか
なぜ、テストが書きやすかったのか?
それは、当たり前ですが、そのクラスが必要なパーツを、外部からカスタマイズできるようにしたからです。
そのため、データの変更を外部から行うことが可能になっています。
もうひとつは、参照元を上位クラス(インターフェース)から行っていることです。
こちらのテストの部分に絞ってクラスの参照を書くと、このようになります。
見ると、共通のインターフェース(ベースクラス)を経由してやりとりしているのが分かりますね。
このように抽象クラス参照しあうのには、メリットがあります。
それは、実体クラスを簡単に変更できることです。
今回の場合は、注入するデータをテスト用データに置き換えました。
だから、テストコードを書くときに、簡単に記述できたのです。
テストが書きやすい = 設計がしっかりしている
テストコードを書いてみて思ったのは、テストが書きやすいコードは、設計がしっかりしているとも取れるということ。
もし、仮にテストが書きづらいものが出てきた場合、設計そのものを見直すべきという、注意サインになるかも知れません。
例えば、テストを書いていて、このようなことが起こった場合。
- 初期化がしにくい … 依存している外部クラスが多い可能性
- テスト項目が多い … クラスに複数の機能を詰め込み過ぎている可能性
- テストが書けない … 依存性のあるクラスが隠蔽されている可能性
これらのケースが出てきたら、メンバーと相談するのが良さそうですね。
テスト駆動開発の準備
では、実際にテスト駆動開発をやりたい場合、どうすればいいか。
実際のツールを確認しつつ、見てみましょう。
Test Runner を使おう
Test Runner は Unity 標準のテスト実行環境です。
2019.2 から、Package 化されています。
実際にテストしてみよう
実際にテスト駆動開発を始めてみようとした場合ですが、色々な準備が必要です。
前提として、テストを行う環境を作るのに、Assembly Definition Files の設定をする必要があります。
特に、リリース済みのプロジェクトに関しての Assembly Definition Files 設定の追加は、ほとんどの場合、アセットバンドルの再構築が必要になると思いますので、ご注意。
こちらのサイトは、初期設定のやり方から、詳しく記載されていますので、かなり参考になります。
最後に
もし、XFLAG の資料で設計を行うのであれば、やはり、テスト駆動開発は導入するべきだと思います。
やはりテストも含めての事例であります。
今後同じようにマネされる方がいらっしゃいましたら、これが少しでも参加になれば幸いです。