
t0mmy
学習履歴詳細
単体テストの考え方/使い方 10章 読了
やったこと
- 単体テストの考え方/使い方 10章 データベースに対するテスト 読了
学んだこと
ポイント
- データベースをテストするのに必要な事前準備
- データベースのテストに関するベスト・プラクティス
- テストデータのライフサイクル
- テストでのトランザクションの扱い
学び
- DB操作を含む統合テストは、テストとして非常に価値がある
- スキーマ情報も、バージョン管理する
- 「単位作業」という考え方
- 統合テストにおけるトランザクションの扱い
- 「フェーズをまたいだトランザクションの使いまわし」は行わないこと
- 読み込み処理のテストは必須ではない
- リポジトリは、個別にテストするべきではない
- ビジネスロジックの統合テストと、確実に重複するため
- ビジネスロジックの統合テストの中で、一緒に検証するに留める
気づき
- DB操作を含む統合テストは、テストとして非常に価値がある(大事なことなので二回書きました)
- トランザクションは、遅延可能であれば、遅延させた方が良い
- パフォーマンスの向上が期待できる
- Snowflake の構文に、
create or replace
という構文があったことを思い出した- こういった構文を使うことが出来れば、DDLは状態ベースで管理できそう
- 問題は、古いデータを新しいテーブルへ移行する方法
- 「読み込み処理のテストは必須ではない」という結論には、コスパの面もありそう
- 書き込み処理は、高リスクのため、徹底的に検証したい => コスパが高い
- 読み込み処理は、リスクが低いため、そこまで重要ではない => コスパが悪い
- 「リポジトリをテストするべきか」という結論にも、コスパの面がありそう
- 改めて、大事なのは **「最低限の労力で、最大限の効果が得られるテストを作成する」こと
メモ
管理下にある依存をいかにテストするか
- 代表的なものはDB
事前準備
- スキーマを、Gitなどのバージョン管理ツールにて管理する
- 開発者ごとに個別のデータベース・インスタンスを用意する
- 「移行ベース」を用いて、データベースの変更を本番環境へ反映させる
バージョン管理ツールを用いたスキーマの管理
ソースコードと同じく、スキーマ情報をバージョン管理ツールにて管理する。
これにより、スキーマの変更を記録・追跡できるようになる。
- バージョン管理ツールで運用する場合は、隠れた変更が内容徹底する必要がある
スキーマとして、テーブル、ビュー、インデックス、ストアドプロシージャに加えて、参照データも含めること。
◇参照データ
アプリケーションを適切に機能させるために事前に用意しなくてはならないデータのこと。
SQLにおいて、テーブル結合に必要なメタデータなど
CREATE 文に加えて、参照データを格納するINSERT文もバージョン管理すると良い。
開発者ごとにデータベースインスタンスを用意する
開発者全員で一つのDBインスタンスを共有すると、以下の問題が発生する。
- 互いのテストに影響が出てしまう
- 互換性のない変更を加える際、他の開発者の手を止めてしまう
この問題を解決策として、開発者一人一人に、専用のDBインスタンスを立ち上げると良い。
「移行ベース」を用いた本番環境への反映
DBの変更を本番環境へ反映する手段として、「状態ベース」と「移行ベース」の二つが考えられる。
◇状態ベース
開発DBと本番DB の差分を取得する際、「状態の違い」に着目する手法。
ツールを用いて開発DBと本番DBの差分を取得し、本番DBが「最新の状態」になるようなスクリプトを実行することで、本番DBを最新化する。
「差分を埋めるスクリプト」が、バージョン管理ツールで管理する対象になる。◇移行ベース
開発DBと本番DBの差分を取得する際、「最新状態への移行に必要な操作」に着目する手法。
つまりはDDLということ?
状態ベース | 移行ベース | |
---|---|---|
データベースの状態 | 明確になる | 隠れる |
移行の手順 | 隠れる | 明確になる |
データベースの状態が明確になると、「マージ競合」に対処しやすくなる。
移行手順が明確になると、「データ・モーション」に取り組みやすくなる。
◇データ・モーション
既存のデータを、新しくなったスキーマに合致するよう変形すること
データベースへの変更が増えるほど、「データ・モーション」に取り組みやすい「移行ベース」に軍配が上がる。
データベーストランザクションの管理
データベース操作とトランザクションを分離する
責務を、以下の二つに分割する。
- データベースにアクセスし、CRUD処理を完遂するリポジトリクラス
- トランザクションを管理する、トランザクションクラス
上記二つのクラスは、生存期間が異なっても良い。
![[Drawing 2023-05-05 18.14.39.excalidraw]]
実装的には、コントローラ(またはその上位クラス)でトランザクションオブジェクトを生成し、リポジトリに渡す。
public class UserController { public string ChangeEmail()(int userId,string newEmail){ Transaction transaction = new transaction(); UserRepository userRepository = new UserRepository(transaction); //他、必要なリポジトリを生成 // ビジネスオペレーション userRepository.someAction(); ... //全DB操作が正常終了した場合、コミット transaction.commit(); } }
commit処理(≒DBへ変更を反映するかどうかの判断)は、コントローラ側で実装する。
一方で、トランザクション破棄の処理は、インフラ層で実装すると良い。
単位作業(Unit of Work)の導入
◇単位作業
一つのビジネス・オペレーションの中でデータの変更が発生するオブジェクトを全て保持し、オペレーション完了時に、変更を一単位にまとめてDBへ反映するパターン。
![[Drawing 2023-05-05 18.36.52.excalidraw]]
単位作業を導入することで、DBに対する更新を後回しにできる。
これにより、トランザクションの生存期間を可能な限り短くすることが出来、更新データの輻輳緩和やパフォーマンスの向上ができる。
統合テストにおけるデータベーストランザクションの管理
統合テストも、三つのフェーズ(Arrange、Act、Assert)で構成される。
この時、トランザクションや単位作業がフェーズをまたいではならない。
- 言い換えると、フェーズをまたいで、トランザクションを使いまわしてはならない
これは、可能な限り本番環境に近い環境でテストするため。
- 本番環境では、トランザクションや単位作業を使いまわさないよね?
テストデータのライフサイクル
データベースを使用する統合テストは、一ケースずつ実行すること
一ケースずつ実行し、テストケースに応じたテストデータを順次用意していくと効率が良い。
- 特に、統合テストのテストケースを同時実行しようと試みることは、労力に見合わない
一つのテストケースが完了したら、不要なデータを削除すること
いわゆる後始末をきちんと行うこと。
テストケースの実行前に削除する(≒初期化する)のが望ましい。
テスト中のみ、in-memory DBを使う問題点
「テスト用に、SQLiteのような in-memory DBを使用する」といった戦略は推奨されない。
これは、「可能な限り本番環境に近い環境でテストする」という考え方に反するため。
特に、in-memory DBは、そうではないDBと比較して機能面で異なる部分も存在する。
そのため、in-memory DBでテストに成功したからと言って、本番環境で正しく動作するかはわからない。
- 言い換えると、in-memory DBでテストに成功したという結果は、「本番環境で正しく動作すること」を保証できない
まとめると、可能な限り、本番環境で使用するDBと、同じDB製品を用いて統合テストに臨むことが推奨される。
そして、「テストの時だけin-memory DBを使用する」といった戦略は避けること。
- 本番環境でin-memoryDBを使用し、その部分の動作を検証したい場合を除く
テストコードの再利用
テストコードも、保守性や拡張性が大事。
再利用しやすよう、ヘルパーメソッドやコンポジションを導入すると良い。
準備フェーズでのコード再利用
オブジェクトマザーパターンと、テストコードビルダーパターンが代表的。
◇オブジェクトマザー
テストフィクスチャの生成を手助けするクラスやメソッドのこと。
テストの準備フェーズを共通化でき、テストコードの再利用性を高めることができる。◇テストデータビルダー
オブジェクトマザーと同じことを、BuilderパターンとFluent Interfaceにて実現する。
可読性が大きく向上する。
書籍では、オブジェクトマザーの利用を推奨している。
- テストデータビルダーは、ボイラー・プレートコードが増えるため非推奨
実行フェーズでのコードの再利用
トランザクションの生成処理が主な再利用対象。
トランザクションブロックをヘルパーメソッドメソッド化し、トランザクション内で実行したい処理を引数として受け取るようにする...など
確認フェーズでのコードの再利用
「確認のため、DBのデータをクエリする」ような処理は、十分共通化可能。
共通化部分は、Fluent Interface で実装すると、可読性が向上して良い。
データベースを使用したテストに関するよくある疑問
疑問1 読み込み処理をテストするべきか?
必須ではない。
- 読み込み処理は、書き込み処理ほどリスクが高くないため
- 非常に複雑な読み込み処理や、ビジネスロジックにとって非常に重要な役割を担う場合はテストする価値あり
また、読み込み処理はカプセル化しない(≒SQLを直接叩く)方が良いことがある。
- 本来カプセル化は、データが壊れないことを保証するための仕組みであり、書き込み処理のための仕組み
- 読み込み処理の場合は、データを破壊することはないため、カプセル化は必須ではない
- カプセル化しないことは、不必要な抽象化層を経由しないことを意味する
- 不要な層を経由しない(≒処理が少なくて済む)ため、パフォーマンスの向上が見込める
疑問2 リポジトリをテストすべきか?
リポジトリは、コントローラに分類される。
結論として、「リポジトリ専用のテスト」は作成するべきではない。
- 「外部のデータストア」、つまりプロセス外依存を専門に扱うため、保守コストが高くつく
- テスト内容が、ビジネスロジックを検証する統合テストと確実に重複する
そのため、統合テストの一部として検証するにとどめる。
同じ理由で、EventDispatcherクラス(ドメイン・イベントを基に、管理下にない依存への呼び出しを行うクラス)もテストしない方が良い。
まとめ
- スキーマはソースコードと共にGitなどのリポジトリで管理する
- スキーマ : テーブル、ビュー、インデックス、ストアドプロシージャなど
- 参照データ : アプリケーションを適切に機能させるために、事前準備する必要があるデータ
- スキーマの一部
- 対義語は通常データ
- 開発マシン上で、開発者個別のDBインスタンスをホストできると、テスト効率が向上する
- データベースの変更を本番環境へ反映する方法には、「状態ベース」と「移行ベース」がある
- 状態ベース
- データベースの比較ツールを用いた方法
- マージ競合が発生しても、解消しやすい
- 移行ベース
- どのようにデータベースを新しい構造に移行するか、に焦点を当てた方法
- データモーションを行いやすい
- データモーションの重要度が高いため、「移行ベース」がおすすめとのこと
- 状態ベース
- 可能な限り、単位作業パターンを採用する
- すべてのデータ更新をビジネスオペレーションの最後まで行わないようにする
- パフォーマンスの向上につながる
- DBが絡むテストでは、準備・実行・確認フェーズでは、それぞれ個別のデータベーストランザクションを持つこと
- 言い換えると、フェーズをまたいだトランザクションを使用しないこと
- テストの後始末は、テスト終了後ではなく、テスト開始時に行うと良い
- 個別に後始末のフェーズを設ける ... といったことが不要になる
- テストの際は、本番環境と同じDB製品を使用すること
- 本番環境ではMySQL 、テストではSQLite ... といったことはダメ
- SQLite のテストであり、MySQLを使用したコードのテストとはならない
- テストコードから、ビジネス的に重要ではない技術的なコードをプライベートメソッドやヘルパークラスに抽出する
- テストケースのコードを量を減らすことが出来る
- 準備フェーズでは、テストデータビルダーよりもオブジェクトマザーを使用する
- 実行フェーズでは、コントローラのメソッドへの呼び出しを委譲するメソッドを用意する
- 確認フェーズでは、Fluent Interface を導入する
- テストケースのコードを量を減らすことが出来る
- Read/Write テストは、 Write の方が重要
- Readのテストは、複雑なクエリを含む読み込みや、ビジネス的に非常に重要なデータの読み込みを中心に行う
- リポジトリパターンを導入したコードは直接テストしない
- 労力に見合わない
- 統合テストの際に、テストの一部として間接的に検証する
2023年05月05日(金)
3.0時間