こちらの講演時にいただいた質問への回答です。

「テスト自動化とテスト駆動開発」講演資料 - やっとむでぽん

質問9. DBアクセスを含む処理をテストする場合、どのように環境を整えるのが良いと思いますか? 例.開発環境DBにつなぐ/クライアント端末にDBを建てる/メモリ内で完結させる

挙げていただいた例は、いずれも有用で、使い分けるとよいです。使い道を分類すると、どれも使うことになるのではないかと思います。使い道、目的に合わせどれを選ぶか、どう運用するか、私の経験から考えてみます。

まずテストの実行時間です。テスト駆動開発のサイクルで使うテストは一瞬(1秒以内)に結果が見たいので、本物のDBは避ける傾向にあります。DBアクセスする部分をモックで置き換えてやれば、実行時間は本当に一瞬(0.1秒以内)になります。これがCI/CD環境で実行するテストであればもっと遅くても構わないので、テスト用のDBに接続して実際のDBアクセスをします。もっとも今どきの環境は、クライアント端末にローカルのオンメモリDBを立てても、実行速度には遜色ないでしょう。フレームワークがそういうテスト用機能を提供している場合もあります。モックの濫用はテストの保守性を下げるので、カジュアルにDBを使うのも選択肢になると思います。

テスト対象も大事です。DBアクセスを含む処理の、なにをテストしたいのか。SQLそのものなら、DB接続は必須です。複雑なクエリを組み立てる部分のテストなら、クライアント端末にオンメモリDBを立てて実行できます。本番準拠のデータからクエリでデータが正しく取得できるか確認するようなテストなら、テスト用DBにデータを準備するほうが経済的かもしれません。

いっぽう取得したデータに対して実行するロジックがテスト対象なら、DB接続はせず、取得したのと同じデータをテストケース内で作って、それを渡してやればすみます。こうした細かな部分を個別にテストを書いていると、ソフトウェアの設計が自然と細粒度になり、依存関係も分離できた状態になります。テストデータのバリエーションも、テストケース内のほうが細かく大量に作りやすいかもしれません。

テスト対象がエンドツーエンド、ユーザーが操作する画面や、バッチの入力ファイルと出力ファイルを対象とするテストならば、当然DB接続は必要です。パフォーマンスを見るなら本番同等のDB環境を使います。

以上のように、テストの種類によって接続するDBも変わってきます。どの環境でどの種類のテストを実行するか考えてみると、一例として以下のようになります。

  • 開発中(テスト駆動開発) ― 速いテストだけ、ユニットテストだけ実行する。DB接続しないか、ローカルDBを使う。
  • 開発の区切り(コミット時、プッシュ時、マージ時、など) ― 手元で実行できるテストをすべて実行し、壊していないか確認する。適当なデータがあるDBに接続するか、ローカルにそういうDBを(テスト時だけ)起動する。
  • CI/CD ― ビルド環境上ですべてのテストを(システムテストや受け入れテストも)実行する。テスト用に用意した、本番同等のRDBMSに接続する。
  • 手動テスト ― 開発中に動かして動作確認したりするときには、手動テスト環境を使う(専用環境でもいいし、ローカル環境も使える)。適当なデータが入っていないと使いにくい。開発中のプログラムから使うので、データが壊れたり異常になったりするのを想定して、運用する。テスト担当者も触るという場合は、さらに別の安定した環境が必要になることもある。

テストデータの問題も大きいです。以下のような作戦があります。

  • テストデータを詰め込んだ「テストDB」を用意し、そこに接続する。手動テストでは、使えるデータを見つけたり作ったりしてテストを実施できる。自動テストの場合、テスト内で変更したデータを再度テストに使うと、テストの事前条件が異なるので結果が変わってしまう。そのため、自動テストを実行するたびに初期テストデータをロードする必要がある。初期テストデータは、テストの内容や機能によって変わっていくので、ソースコードと同じくバージョン管理対象になる。開発中に手動で動作確認することもあるので、それ用の「開発DB」と、自動テスト用の「テストDB」は、別々に用意する。複数人が同時並行してテストを実行することを考えると、開発者1人にテストDBを1つ用意するほうが安全(DBMSとしては1つで、ユーザーや名前空間で分離してもよい)。
  • テストデータが「全テーブルの全レコード」で構成されていると、いろいろな条件でテストしたいときにバリエーションが足らなくなる。そのため、個々のテストケースについて、そのテストのためのテストデータを構築する。テストケース内でプログラムでデータを生成してもよいし、ExcelCSVで別ファイルとして用意しておく方法もある。プログラム内で生成すると、「どんなデータにどんなテストをするか」が一緒に書いてあって読みやすくなり、変更もしやすい。いっぽう、複数のテーブルに渡る複雑なデータ構造を組み立てるのに、プログラムでは煩雑になり過ぎることも多く、データを外出し(別ファイル)にするのもよい。
  • プログラムをテストコードから呼び出してデータを生成したい、しかし煩雑になる、という場合に、テストデータを構築するためのヘルパーライブラリを作るという方法もある。またRuby on Railsのfactory_botやLaravelのSeederのように、フレームワークサードパーティのライブラリとして、そうした機能が提供されているものもある。
  • 別の観点として、アジャイルではデータスキーマも頻繁に変更するため、DDLもバージョン管理対象とし、テスト実行時にバージョンを確認したり合わせたりしないといけない。フレームワークマイグレーションスキーマバージョン管理の機能を提供していることがある。

プロジェクトのテスト戦略として、どういう部分をどういうテストでカバーするか計画し、漏れがないようにしましょう。たとえばDAO(Data Access Object)を、ユニットテストでは「統合テストで確認するから」とモックで置き換え、統合テストでは「ユニットテストで確認してるから」と網羅しなかったりすると、テスト漏れになってしまいます。逆に、システムテストで「絶対確実だからここで全条件網羅する」などと考えると、テスト量も実行時感も膨大になり、費用対効果が悪くなります。

いろいろ書いて、整理ができておらずすみません。私がいいなと思う一例は、こんな感じです。あんまりちゃんと考えられてないので、おかしなところがあると思いますが……DBを青く塗っています。ここは私も学びたいので、ツッコミやコメントをお待ちしています。

f:id:yach:20210409105449p:plain