ユニットテストについての少考
ユニットテストの効用をリグレッション防止・ドキュメンテーション・内部構造の洗練の 3 つに分けて整理し、実務でどこまで書くかを考えたものです。
- 公開日
この記事は別サイトにも掲載しています。 https://iganin.hatenablog.com/entry/2019/12/26/232531
はじめに
今年読んだ以下の本の中で設計におけるユニットテストの立ち位置を語る箇所が多くみられました。「レガシーソフトウェアからの脱却」、「レガシーコード改善ガイド」、「CAREER SKILLS」、「クリーンアーキテクチャ」など。実際に仕事の中で使ったり、考察を行う中で自分なりにユニットテストの有用性や実務での妥当な利用方法を考察しました。
本稿はそれらを踏まえた上での現状の私の考えです。業界のスタンダードからは認識がずれている可能性がありますし、基本的には自身の思考を整理するための手段として本稿の文章を書いています。
ユニットテストの効用
ユニットテストの効用は大きくはおそらく以下の 3 点です。
- リグレッションの防止
- ドキュメンテーションとしての役割
- 内部構造の洗練化
リグレッションの防止
ユニットテストを書くことによってリグレッションが発生した際に気づくことができます。例えば既存クラスになんらかの変更を加える際に、特定の条件で true と返却するはずが、その変更によって false が返却されるようになっていた場合、既存の処理に悪影響を及ぼしていることになりますが、該当処理にユニットテストを作成していればこのことに気づくことができます。
また、ユニットテストによるリグレッションの防止が達成されている場合、リファクタリングをより容易に行うこともできるようになります。リファクタリングは ふるまい を変えずに内部処理をわかりやすくしたり、効率的にすることとします。ここで ふるまい を変えずに、という点がポイントになります。
ユニットテストは該当箇所のふるまいをテストできます。よって、ユニットテストがある状態でリファクタリングを行い、ユニットテストが問題なく通った場合はリファクタリングによってふるまいが変わっていないことが保証できるため安心して効率的に作業を行うことができます。
リグレッションを防止する、もってリファクタリングの効率性を向上する、これがユニットテストの 1 つ目の有用性であると考えています。
ドキュメンテーションとしての役割
ユニットテストは対象のメソッドに対する ふるまい の仕様を表すドキュメンテーションとして機能します。エッジケースの際にどのように振舞うのか、発生しうる分岐パターンは何か、そういったことがユニットテストから把握することができます。
コードから独立しているドキュメンテーションは時間の経過とともに実コードと解離が発生しがちですが、コードの作成とともに実装、修正されるユニットテストであればコードからの解離は原理上発生しづらく、有効なドキュメンテーションとして機能します。
内部構造の洗練化
ユニットテストを実装できるようにコードを実装する行為によって内部構造が洗練化されます。ユニットテストを実装できるようにするためには、クラス間が疎結合であり、影響の範囲が限定されていなければなりません。また、必要のない場合は外部にインターフェースを公開せず、該当処理や状態を内部に閉じ込める必要があります。必要な依存関係に関しては依存関係の注入をできるようにすることでモックと入れ替えられるようにする必要もあります。なぜならば、そうしなければ依存先のコードの実装に依拠してしまい、ユニットテストにならないためです。
CLEAN と SOLID 原則という言葉があります。CLEAN はコードレベルでの指針を指し、Condensed, Loosely Coupled, Encapsulated, Assertive, Not redundant の頭文字をとった言葉です。(記憶がおぼろげなので各英単語は間違っているかもしれません)。凝縮されており、疎結合でカプセル化され、冗長性がなく、断定的であることをもって運用・保守性の高いコードであるという内容だったかと思います。
SOLID 原則はアーキテクチャレベルの内容です。SRP(Single Responsibility Principle - 単一責務の原則)、OCP(Open Close Principle - 開放閉鎖の原則)、LSP(リスコフの置換原則)、ISP(インターフェース分離の原則)、DIP(依存関係逆転の原則)を指します。
ユニットテストを作成しやすいようにアプリケーションを作成していくと上記の CLEAN で SOLID 原則に則ったコードになりやすいと考えています。ユニットテストを書きやすくするためには各ユニットに対して依存関係を少なく、テストが必要なインターフェースを少なく、非冗長なコードにする必要があるためです。
ユニットテストを実装しながらプロダクトコードを書くことで、CLEAN で SOLID 原則に沿ったコードを作成しやすくすることができると考えます。
アプリ開発でどのように活かすか
アプリ開発で上記の効用を全て得るための方法を考えます。おそらく新規のアプリ開発と既存アプリへの適用で方法は変わります。本稿では新規アプリ開発に焦点を当てます。
効用全てを得るためには、テストをアプリのコードを作成してから作成するのではなく、アプリのコード作成と並行してユニットテストも作成すると良いのではないかと考えています。具体的には、アプリ側でふるまいとして Interface を切る、その Interface に対してユニットテストで期待するふるまいを実装する、テストに通るようにアプリ側のコードを埋めるというような流れです。いわゆる TDD の方法をとるのがユニットテストの効用を得るには最善なのではないかと考えています。(BDD Behavior Driven Development という言葉もあるようですが、こちらはあまり詳しくありません)
ただ、毎回全て手動で作成するのは大変なため Generamba のようなコードジェネレーションツールを使用して、アプリのコードと同時にユニットテスト用のガワを作成するのが良いと思います。MVVM、Viper、Redux などのアーキテクチャに沿って、必要となるコードのセットを作成するようにテンプレートを作成するといった方法です。
このようにすることでコードジェネレーションツールを使用するかぎりにおいて、実装とユニットテストの作成を同時に進めることを半強制することができ、ユニットテストの効用を全て得ることができるのではないでしょうか。
とはいえ
上記までの考えが今現在考えているユニットテストのメリットを得るための開発方法ですが、とはいえなかなか大変でもあると考えています。また、全ての開発をこの方法で行うべきとも考えていません。PoC 段階のアプリでそのアプリベースから発展させることを意図していない場合は NoCode でさくっと作ってマーケットの反応を見た方が良いでしょうし、メンバーのレベルによってもとりうる方法が変わります。
新規開発であれば上記の方法で良いでしょうが、既存の大量のコードがある状態からのユニットテストの導入をする場合ではこうもいきません。(この辺りはレガシーソフトウェア改善ガイドが大変参考になりました)。結局のところ都度都度状況に応じた最適解を選択できるよう考える必要はあるかと思います。
最後に
現状の自分の考えをまとめてみました。あくまで今はこう考えるというものであり、今後の経験やより多くの知識を得る中で変わることもあるかと思います。また絶対的に正しい考え方だとは思っていません(往々にして正しさは適用する対象と状況に応じて変化しえます)。最近得た知識をまとめるために記載しましたが、これはこうなんじゃないか、といったご意見がありましたらコメントいただけますと幸いです。