この記事について
「単体テストの考え方/使い方」(Vladimir Khorikov著、須田智之訳、マイナビ出版、電子版ver1.00、2022)についての覚書と感想です。
ポイント
- 良い単体テストの条件は、退行に対する保護を与え、リファクタリングで壊れにくく、迅速に実行とフィードバックを回すことができ、保守がしやすいこと。
- 単体テストは観察可能な振る舞いを検証し、実装の詳細を知らないようにせよ。(原則としてブラックボックステスト)
本書は何でないか
- 品質保証の本ではない。
- どのように設計の正しさを担保するかは書いていない。
- テストフレームワークの本ではない。
- 本書のコード例はC#とxUnitで書かれているが、言語もフレームワークも深入りしない。
- チーム開発の本ではない。
- 開発サイクルにテストを取り込む必要性は説くが、その実践的なワークフローや継続的インテグレーションには触れない。
単体テストの定義
自動化されていて、次の3つの性質をすべて備える者が単体テストとなるのです。
- 「単体(unit)」と呼ばれる少量のコードを検証する
- 実行時間が短い
- 隔離された状態で実行される
- P.28
雑記
この単体テストの定義は、日本のいわゆるシステム屋の現場の(開発工程としての)「単体テスト」とは意味が違う点、注意が必要。
現場の「単体テスト」は、以下のような性質であることが多い。
- 詳細設計書とコードベースが一致していることを確認する
ウォーターフォールの工程として作ったコードが詳細設計通りに動いていることを検証するのが単体テストという扱いになっている。そこに継続的な開発は意識されない。 - 単一の機能を検証する
設計は機能単位の設計書が多く、したがって機能ごとにテストすることになる。 - ホワイトボックステストである
品質の数字が欲しいので網羅率100%付近を望む傾向があり、必然的にホワイトボックステストになる。
このような「単体テスト」の1,2の性質は、むしろ本書で言うところの統合テストのほうが近い。
単体テストの目的
適切な開発スピードを維持し、プロジェクトの成長を持続可能にすること。↔適切にテストされないプロダクトコードは変更困難になる。
良い単体テストとは
目的のために単体テストが備えるべき性質
- コードベースを変更したとき退行(regression,デグレ,バグ)を検出することで、リファクタリングや新たな機能の追加を簡単に行えるようにする。
- また、テストも保守コストがかかるので、効率的で保守しやすい最小限のテストコードにするべき。
- 常に早いタイミングで検出するために、自動化し、開発サイクルに組み込まれていること。
単体テストの取捨
テストコードを含めてコードは保守コストを生じる負債である。コストに見合う価値がないなら、無いほうが良い。
そこで、よくテストすべき部分=単体テストする部分とそうでない部分のメリハリが必要。
バグが入り込みにくい単純なコードは単体テストの対象としない。
コードベースの重要な部分は単体テストされなければならない。重要でない部分はそうでもない。だから重要な部分が隔離された構造が欲しい。
重要な部分はたいていの場合ビジネスロジック。そのような設計には、ドメイン・モデルが必要。なのでDDDやヘキサゴニカルアーキテクチャへの言及がある。
単体テストの対象
検証対象とする単体とは 1単位の振る舞い である。
1つのクラスを単体とする考え方もある。本書は1単位の振る舞いをテストするほうを良しとしている。1つのクラスを単体とする(そのクラスへの依存性全てをモックする)と、テストコードが実装の詳細に立ち入ってしまい壊れやすいテストになる。
観察可能な振る舞いと実装の詳細
第五章5.2。個人的にこの本で一番重要なチャプターだと思う。
しかし単体テストというよりは、プロダクトコードの設計の話。
単体テストでは(統合テストでも)、コードを観察可能な振る舞いと実装の詳細に分け、観察可能な振る舞いをテストする。
観察可能な振る舞いについて本書は次のように定義している。
システムの観察可能な振る舞い(observable behavior)の一部になるには、そのコードは次にあげるもののどちらかでなければなりません。
クライアントが目標を達成するために使う公開された操作 ―― ここで言う操作とは、計算をしたり、副作用を起こしたりするメソッド、もしくは、それら両方を行うメソッドのことである。
そして、これら2つに該当しないコードが実装の詳細(implementation detail)となります。
クライアントが目標を達成するために使う公開された状態 ―― ここで言う状態とは、システムの現時点でのコンディションのことである。
- P.140
ここ、「クライアントが目標を達成するために使う公開された状態」の説明がピンとこないが、何かしら参照する操作と理解しておく。_systemctl status_コマンドみたいな。
ドメイン層とアプリケーション・サービス層はそれぞれ観察可能な振る舞いを提示し、独自の実装の詳細を内部に保持するようになっています。
- P.141
ドメイン層、アプリケーション・サービス層は、ヘキサゴナルアーキテクチャのそれ。
- ドメイン層のクライアントがアプリケーション・サービス層
- アプリケーション・サービス層のクライアントがユーザーや外部アプリケーション
ドメイン層の観察可能な振る舞いが単体テストの単体になるし、アプリケーション・サービス層の観察可能な振る舞いが統合テストの対象になる。
観察可能な振る舞いをテストする=実装の詳細をテストしない理由は、実装の詳細はリファクタリングで変わりうるため。リファクタリングで壊れるテストは、リファクタリングの邪魔になり、目的に対して逆効果になってしまう。
一方で、コードベースでは「公開されたAPI」と「プライベートなAPI」を区分できるので、公開されたAPIが観察可能な振る舞いを表現するようにする、ということを述べている。
これは言語によって提供されるAPIの方法を指しているようだ。しかし単にアクセス修飾子をpublicにすれば公開されたAPIだという記述は、どうなんだろう。
publicでもドメイン層の観察可能な振る舞いであるメソッドと、ドメイン層の外から呼び出してほしくないメソッドを区別する必要があるのでは?
internalとかpackage-privateといった細かいアクセス修飾子で公開範囲を調整…するのは、たぶん破綻する。
とりあえず思いついたのは2つ。
-
インターフェースをかぶせて公開されたAPIとして扱う。ただこれは不必要なレイヤー化になりそう。
-
APIリファレンスで公開を意図しないメソッドにはその旨を明示。
ところで、1の図のような構造では、DomainClassCも普通は単体テストの対象にすると思う。たとえアプリケーション層から見て実装の詳細でも、クラスを切った以上は独自の役割があるはずなので、それを直接的に検証しないというのは気持ち悪い。筆者が単体をクラス単位にしないと言っているのは、DomainClassBをDomainClassCから切り離してテストすると余計なスタブが必要になるからという話で、逆はアリだと思う。
管理下にある依存と管理下にない依存
管理下にある依存(managed dependency) ―― この依存はテスト対象のアプリケーションが好きなようにすることができるプロセス外依存である。外部アプリケーションが管理下にある依存にアクセスするには、必ずテスト対象のアプリケーションを経由しなくてはならないようになっている。(中略)典型的な例にテスト対象のアプリケーションしかアクセスしないデータベースがある。
管理下にない依存(unmanaged dependency) ―― この依存はテスト対象のアプリケーションが好きなようにすることができないプロセス外依存である。(中略)典型的な例にメール・サービスやメッセージ・バスがあり、両方とも他のアプリケーションに見える副作用を発生させる。
- P.269
統合テストつまりアプリケーションに対するクライアントの視点から見ると、管理下にある依存とのコミュニケーションは実装の詳細となる。
統合テストでは、管理下にない依存はモックするが、管理下にある依存はモックに置き換えない。
雑記
本書は業務ロジックとSQLの関係に触れていない。
SQLに業務ロジック含めてもいいよ派なので、「管理下にある依存」としてのDBを含めて単体テストかそれに準じるテストを行えないか考えている。つまり業務ロジックに対するテストの一貫としてSQLもテストしたい。
さて、単体テストが満たすべき性質は…
- 「単体(unit)」と呼ばれる少量のコードを検証する
- 実行時間が短い
- 隔離された状態で実行される
1は、1単位の振る舞いを単体とする立場では、SQLだってドメイン層の1単位の振る舞いの一部なので繋げてテストするのはむしろ自然と思う。
2と3を満たすのが難しい。
3を満たすには、ケースごとにDBをセットアップするのが最善だが、それは2が満たせない。そうでなくともDBに繋ぐだけで実行時間は大きく増加する。
インメモリデータベースで代替する機能のあるフレームワークとか使えば速度は稼げそうだが、できるとしてもORM頼みでSQLの機能を使えないでは本末転倒。本書でもインメモリデータベースは機能的に異なる部分があり、テストが正確でなくなる可能性があるとして推奨していない。
それからDBのデータを自動的に投入・検証する方法は、そもそも複雑1だし、テストコードも冗長になりテスト自体の保守コストを上げるという問題もある。ままならない。
SQLに重要なロジックがあるなら統合テストでリポジトリサービスを分けてテストするべきか。
あるいは、SQLに業務ロジック含めるのダメ派に転向するか。
気に入ったトピック
アプリケーションを構成する層を減らす
バックエンドのシステムであれば、ドメイン層、アプリケーション・サービス層、インフラ層の3つの層だけで十分に構成できる。インフラ層にはデータベースのリポジトリ、O/Rマッパー、メール・サービスのゲートウェイなどが含まれる。
- P.286
無駄なレイヤー化というものは本当に邪魔なので分かる。
本番DBへの変更は「移行ベース」
DBの変更とは、スキーマや参照データの変更。
参照データ(reference data)とは、アプリケーションを適切に機能させるために事前に用意しなければならないデータのことです。
- P.330
変更の本番DBへの適用方法は2通り。
状態ベース:開発用DBと本番DBのスキーマ・参照データの現新差分をDB用の差分ツールで作成して、本番DBに適用する。
移行ベース:開発用DBへのスキーマ・参照データの変更を(データ・モーションも含めて)ソースコードとして積み上げ、本番DBに適用する。
データ・モーション(data motion)とは、既存のデータの形状を変え、新しくなったスキーマにそのデータが合うようにすることを指します。
P.334
データ・モーションの例としては「氏名」列を「苗字」「名前」列に分けるなど。
移行ベースのほうが、本番データのデータ・モーションで有利なので推奨。
クラウドで言うインフラのコード化に通じる話ですね。
TODO
- DDD本をちゃんと読み直す。
- テスト駆動開発を読む。
- 関数型アーキテクチャについて知る。
-
エスケープ文字、日付型、固定長文字列のパディング…色々ありますデータのトラブル。 ↩