久しぶりに C# での開発現場に戻ってからテストコードを書く時にやっていること - Respawn を使う -

dany1468.hatenablog.com

上記の記事の続きです。

RDB を使ったテストを書きたい

Ruby on Rails ではデフォルトで Model に対するテストでも RDB を使ったテストを書くことができます。

一方で、私が担当している C# コンソールアプリはテストも無いので当然ながらそういう仕組みはありません。

そこで、まずは以下を目標にしました。

  • テストの AAA でいうところの Arrange でテストデータを RDB に物理的に入れる
  • テストの実行後に RDB からデータを全て削除する

Rails では DB Transaction を使う方法もありますが、今回のアプリは Npgsql を使って直接 SQL を実行するような感じなので、 DB Transaction をテスト時に操作するのは困難であったため、上記のように単純な手法を選ぶことにしました。

この記事でフォーカスする課題は、上記目標の後者であるテスト実行後のデータ削除に関してです。

DatabaseCleaner が無い

Rails を使っていると DatabaseCleaner/database_cleaneramatsuda/database_rewinder を使って、テストの実行後にテストで利用したデータを削除する仕組みを簡単に作れます。

社内の他の C# のコードの中には一部 RDB を利用したテストを書いているケースがあるのですが、そこではおそらく複数人で共通の RDB を開発用に使うためか、明示的に削除するためのコードを書いているものばかりでした ( CI を実行する場合も同様)
もちろんそれでもいいのですが、Rails の現場に居たことで、その手法が面倒に感じてしまいました。

Respawn を使う

github.com

README より引用
Respawn is a small utility to help in resetting test databases to a clean state. Instead of deleting data at the end of a test or rolling back a transaction, Respawn resets the database back to a clean checkpoint by intelligently deleting data from tables.

Respawn は Checkpoint と呼ばれる削除のための定義をコード上で作成した時点で以下のコードにあるようにテーブルの一覧を取得し、加えて対象となる schema や削除対象の絞り込みを行います。

https://github.com/jbogard/Respawn/blob/master/Respawn/DbAdapter.cs

database_rewinder が INSERT を監視して、追加されたデータだけを削除するのと対比すると、 Respawn の削除はテーブル単位であるため、細かく削除定義を設定しないとテストに全く関係の無いテーブルのデータも削除することになるので無駄があるように見えます。
一方で、テストにおいて速度を考えるのはそれが問題になってからで良いと思うので、ひとまずは私の課題は解消されました。

現状は schema ぐらいしか削除定義では指定しておらず、テストに関係の無いテーブルも対象にしてしまっていますが、速度に問題が出てくるようになればテスト毎に削除定義を細かく設定することになりそうです。
ただ、そうなるとテストコードに「このテストはこれらのテーブルに依存している」というのをモロに記述することになるので、実装が漏れ出しているように感じてしまいますが、そこは仕方ないのかなと思います。

他の方法

Npgsql を利用する前提で考えると、 Npgsql が LoggingProvider を登録できるので、 INSERT を監視する LoggingProvider を作成し、そこからテスト後に削除すべきテーブルを取り出すのもいいかと思っています。

テスト対象が Entity Framework のような ORM を利用していれば、Hook を利用するなど、取れる方法が広がるのかもしれません。