DBが関わるテストコードについて、私の場合

www.mizdra.net

皆さんがどうしているのか知りたい!

私も知りたい!!!

ちょっとはてブコメントで雑に書いてしまったので、私の場合をちょっと記事にしてみることにしました。

ただ、特に変わったことはやってないと思います。必要だと思うテストケースなら歯を食いしばって書いたりレビューしたりしています。

なのでこの記事を読む価値はほとんどありません。ご清聴ありがとうございました。

追記: 読み返すとまとまりの無さが酷い。酔った勢いでダラダラ書いてしまった良くない・・・

私の場合(要約)

テストを考慮してない実装についてはDBを使うテストを書いていました。

DBのテストデータは実際には自動生成することはありませんでした。良さそうなら試したいですが基本的にやりたいテストケースがあって書くのでランダム値を使おうとは思わないです。

DBの依存をRepository層としてインタフェースに切り出した時は、RepositoryのテストはDBを使い、そこ以外はモックを使いました。

Repositoryのテストの場合は前処理でDBレコードをINSERTしたりして用意するのですが、Repository単位だと依存するテーブル数もそう多くなく、比較的見通しがよかったです。また、TRIGGERやストアドプロシージャを使ったテストも同様にRepository単位で一緒にやっています。一時期「TriggerやプロシージャはDB製品のロックインを生むから悪!ちゃんとアプリのコードで書け!」といった風潮があった気がしたのですが(具体的な出典がないので思い込みかもしれませんが)やっぱり便利機能だと思うので使っていくのが良いと思った次第です。

RepositoryをモックしてRepositoryに依存のしている他のテストをすると、DBを使うテストよりも実行時間は短く済みました。 しかし、SELECTを行うようなメソッドの処理をモックするには特定のデータを返させるためのコードをかかないといけません。するとテストの前処理が長くなったり、そのテストケースになる意図が伝わりにくかったり、そもそもモックのテストデータが誤っているというようなことがあり、若干しんどい思いをしています。なお、SELECTもINSERT系は引数が正しく与えられたことを検証するようにしています。これをモックライブラリではなくテスト用の実装を作るのは結構難しいのではと思ったりしています。

歯を食いしばってやっていくしかないと思ってやっています。

ただ、一方でモックでいいのか?というのは今も悩んでます。

blog.8-p.info

この記事をみて、そうだと思いつつ、DBに関しては扱うデータ量とテストケースの都合によってはそうとは言えない気がしています。

まず、DB操作をRepository層として抽象化した場合、Repository層に依存するコードのテストは以下の3つのいずれかの方法で準備する必要があるかと思います

  • 実際のDBを使ったRepository実装を使う
    • 事前にINSERTしたりするのが辛い(セットアップのコードがとても辛い)
  • テスト用にRepository実装を用意する
    • 実装のミスが気になる
    • 引数の検証を書こうとするとしんどい
    • どういったデータを返すかを考えるのが面倒(テスト用実装の中で返すか、コンストラクタ引数か何かで受け取るのか)
  • モックライブラリを使う
    • 引数の検証もできて便利
    • やっぱりセットアップのコードが辛い

この中で一番マシだと思ったのがモックライブラリでした。

モックを使ってRepository依存のコードを検証でき、Repository実装は実DBを使って検証するので、まぁさほど酷い結果にはなってないのではないか、と思ってます。

でもモックを使うとモックのテストをしている感じになってしまい辛い。モック結局どうなんだ…

あとDBの並列化についてはやりたいと思いつつも、テスティングフレームワークの気持ちがわからずうまく実装できずにいます。スキーマなりデータベース単位でコネクションを用意してプールしてセマフォで同時実行数を管理すればいいだけでしょ?と思ってるんですがうまくいきません。もっと一般的なライブラリがあってもいい気がしています。

レビューは人のコードの意図が分からない時は聞きながらやってます。。ただ、大筋はほぼ同じで1回説明を受ければ大体読み解けるので頑張ろうという気持ちでやってます。

11/25 追記:

Prisma で本物のDBMSを使って自動テストを書く - mizdra's blog

データアクセスレイヤのテストを書く際にDBをモックするのは自作自演のテストになりがちなので個人的にはおすすめしません

2022/11/25 08:22
b.hatena.ne.jp

自作自演めっちゃ感じます…モックが無くても書きやすく読みやすくなるテストコードになるようもっと精進します…

11/26 さらに追記

色々な方のコメントやツイートを眺められて良いですね・・・。議論を生み出してくれたmizdra先生ありがとうございます

(とても頷いている)

そして私はt-wada先生の「データアクセスレイヤのテストを書く際に」を完全に見落としていた…。Repository層でも色々処理をやるだろうからそのテストはモックしてもできるだろうけど、肝はDB操作などのデータストア操作のはずなので、そこをモックすると意味がないということ…のはず。私もそう思います。

自分が「自作自演を感じる」のはRepositoryに都合のいいデータを返させるようモックで実装する点でした。DBがあればDBにデータ突っ込んでおけばDBが勝手にやってくれるところを、DBを使わずモックさせるとなると自分で期待値を用意しないといけないのがモヤモヤしてたという話。(モックなんだからそういうものなんですが)

Repositoryのテストはモックを使ってもできるんですが、そもそもDBの振る舞いをモックするのはそれだけで苦行なので辞めたほうがいいです。頑張るとできちゃうんですよ。なんでもモック病を拗らせてしまった方は分かるでしょう…

特に動的型付け言語のモックライブラリやJavaのモックライブラリ(Mockitoとか)は強力なので、本当になんでもモックが書けます。C#のMoqはinterfaceのモックしか書けないので嫌でもinterfaceを通して操作する形にすることになります。最初はそれが不満で、例えばAzure関連の開発には公式のSDKが出ていますがほとんどinterfaceないせいで「これどうモックすんだよ」ってブチ切れていました。

しかし、面倒でも1枚インターフェースを挟んであげてこっちでただ呼ぶだけの実装を書いてコントロールできるようにすればよいことに気づきました。Repository層とかはそういう層なんだと思います。

11/26 さらに追記

(データベース OR DB) (モック OR mock) - Twitter Search / Twitterを見てて、思うところがあったので。

文脈が隠れていそうな感じはしますが「DBをモックする」ことに否定的な意見が多くみられました。

データアクセスレイヤのテストを書く際にDBをモックするのは自作自演のテストになりがちなので個人的にはおすすめしません

おそらく、t-wada先生のこの一部を切り出してしまっている感じがしますね…私も最初そう受け取ってしまった…

「データアクセスレイヤのテストを書く際にDBをモックするのは~」とある通り、これは「本来DBを使ってテストすべきところをDBをモックするのは意味がない」という意味でしょう。(でも「個人的におすすめしません」ではなく「すべきではない」ぐらい強く否定してもよいと私は思いました。何の意味もないですから)

t-wada先生は常に正しい方向に導いてくださる(盲信)

本番ではDBを使うので、テストだってDBを使ってテストするのが一番いいのは間違いない。そこはブレないと思います。

DBを使ったコードを書いているならそこはテストしなければなりません。そしてDBは自前で用意できることが多いはずですから、実際に動かすのが良いでしょう。だからモックする必要はないはずなんです。だって実際に動くモノがあるのだから。

しかし、現実問題としてはDB等の外部依存は通信が発生する分遅いし、同時実行でうまく動かないので排他制御が必要です。ファイルIOもそうでしょう。こういうテストが積み重なってくるとテストに時間がかかるようになります。私はこういった実際のDBを使うと時間が辛いので、DBのテストをする箇所とモックを使ってテストする箇所を分けて一時しのぎをしているという感じです。

ここまで書いてやっとこれが言いたかった感が出てきた。

では、DBを使ってテスト書いてて困ったらモックを試してみては?という言い方ができそうなんですが、私が思っているのは、困ったり苦しくなった時はだいたい手遅れで、そこから方針を変えるのはそれなりに時間もかかります。そもそも改善の時間をくれなかったり、ちゃんと数値で示して効果がないとやっちゃダメというところもあるでしょう。

一方、「ではデータアクセスレイヤーを分けしてモックを使えるようにしたらテストは充分早くなるか?」というとそこはまた怪しいでしょう。規模でぶん殴られると死にます(なんでもそう)。

そうなると並列化や分散テストとかが発展して充分早くテストを実行できるようにしていく必要があるでしょう。そうすると、モックを差し込む意味はなくなるかもしれません。そうなるとデータアクセスレイヤーを設けるとどうしても「DBに1発クエリを投げて結果をModelオブジェクトに詰め替えるだけ」という1行の処理を別ファイルに書き続けることになり「これいる?」という感覚になるかもしれません。(今もテストのために必要と言い聞かせて書いてる所はあります)

要するにDBのテストをどうすべきかはケースバイケースで自己責任で、という感じでしょうか(雑)

(以降、それがごちゃごちゃに混ざってる文章になっているかも。整理せずにかいちゃっているので)

追記ここまで

おまけ:DB周りのテストの小細工

と言っても有名なのでそこまで大した内容はないですが…

テスト結果を冪等にするためにテスト開始前にtruncateして空のテーブルにする

Table Truncation Teardown at XUnitPatterns.com

テスト結果を冪等にするためのパターンです。簡単です。

また、終了時にtruncateするのもありますが、個人的には開始前に行ったほうが良いと思っています。というのも、テストがfailしたときにDBにその時の状態が残るからです。

テスト結果を冪等にするために空のテーブルで始めて各テスト開始時にトランザクションを貼りロールバックすると若干早い

Transaction Rollback Teardown at XUnitPatterns.com

次のテストを始める前に一々truncateすると時間がかかります。なのでトランザクションを始めて終了時にロールバックすれば真っ新になります。

truncateするより早いですが、テスト対象がトランザクションを扱う(commitしたりする)場合はうまくいかないのでお勧めしません。

データ量が少ないときはTruncateよりDeleteのほうが早かった(PostgreSQL 11)

よくtruncateは早いとはよく言われるのですが、ユニットテストぐらいのデータ規模だと条件指定がないDELETEのほうが早かったです(私の環境だけかもしれません)

ただ、テーブルに外部キー制約があると依存関係ができているので、DELETEの順序を考慮する必要があります。

非推奨: 全部のテストで使える初期データを用意する

テスト開始時の初回時のみデータを初期化して、各テストでテストデータを使いまわすやり方です。

共通のモノ(例えばユーザ情報など)はテスト用にある程度用意しておき、イベント(例えばユーザの投稿など)は各テストに必要に応じて揃えておきます。後は READ COMMITEDでトランザクションを貼ってテスト後はロールバックすれば、各テストに影響なくテストができます。

これの良いところはINSERTの回数が減るのでめちゃくちゃ早くなります(思いついたときは「俺天才では?」と思ったぐらい早くなります)

欠点はテストデータを作るのが非常に難しい点です。何気なくテスト用に新しいレコードを追加したら例えば投稿一覧取得といった他のテストがこけはじめたりします。あとめちゃくちゃコンフリクトします。(「だからみんなやってないんだな」って思いました)

愚者なので経験から学びます…

非推奨: 互換DBを使う(SQLiteやH2Database,MySQLのENGINE=Memory等)

DBのバックエンドをより高速なものを使う方法です。実際に効果があったり割と知られていると思いますが、個人的な経験上はこのせいでトラブルを生んでいるのでやらないほうが良いと思っています。

これの最悪なのは「期待通り機能したと思ったら実際のDBでは機能しなかった」みたいなケースが起きることです。

EFCoreやActiveRecordのようにバックエンド実装を完全に隠せる範囲で使っているのであればうまくいくかもしれません。

他にも値はレコード&カラムごとに被らないように振りましょう(取り違えた時のミスに気づかない)とか細かいノウハウはありますが、それは今はどうでもよいでしょう(面倒くさくなった)


以降は私の経験した仕事とDBテストに関するお話です。隙あらば自分語り。思い出を振り返りながらなので与太話が多め。話はフィクションだとよいですね。

過去にDBが絡むテストを色々やって苦しみながらも開発を回しており、未だに良かったか、どうすべきだったか、他のメンバーは何を思いながら今のDBテストを書いているのだろう、とかぼんやり考える事がよくあった。

おそらく私がやっているのは3~6人程度のプロジェクトが多く、かなり小規模だと思う。そこそこの規模~大規模のやり方も本当に聞きたい。

やってきたこと

2011~2013: Java SE 6 + MySQL + JUnit4 + DBUnitを使い再現テストを作った(ゴールデンテスト?スナップショットテスト?)

実際にDBを使ったテストを行う方法。プロセスは別途立ち上げておく。

DBUnitを覚えたきっかけになった。

https://www.dbunit.org/

新人~入社3年目ぐらいのとき。2011~2013年あたりかな。初めて参加したプロジェクトはユニットテストがろくに書かれていなかった。当時はCIなんてなく、SVNからチェックアウトした後にリビジョンをチェックして差分がないことを確認したらEclipseの機能でwarを各自で作って商用環境に持ち込んで祈りながらデプロイだった(奇跡的に問題になったことはなかった)その後Antを入れてEclipseレスでwarを作れるようにしてた記憶がある。Antなつかしすぎるだろ。

そのプロジェクトはDBの状態に応じて外部のAPIを操作する定期バッチ処理を動かすもので、基本的な操作はDBの操作だけになるようになっていた。最初はプロジェクト内製ツールでリクエストを飛ばし、DBの状態とレスポンスを目視で結果を確認するということをやっていた。やることは決まってたので自動化してあとでまとめて確認だけ目視でやっていたがそれも辛くなった。

で、当時の上司に辛いんですがみたいな話をしたらJUnitがあるから検証してみてと言われた。しかし「assertEquals(3, add(1,2))とかおままごとじゃん。それよりDBをどうにかしないといかんだろ、JDBCで頑張りたくないぞ、どうすれば・・・」と思いながら探していたらDBUnitを見つけたような気がする。他に無いからあっさりだった。

DBUnitを使うと、テーブルのデータがExcel(2003形式, xls)の1シート1テーブルで書くか、XMLで表現でき、このファイルをそのままDBにインポート(テストが冪等になるようDELETE-INSERTすることもできる)できる機能があった。また、QueryDataSetを使うことで指定したテーブルのIDataSetを取得できるので、それをxls,xmlなどにダンプするといったことができる。

またDBデータの比較DiffCollectingFailureHandler を使ってDataSetの差分を見つけることもできる(SortedDataSetと組み合わせて使っていた気がする) これで期待値の比較を書いていた。実際はJUnitのMatcherを書いたりしていた。

そしてテストの流れはこんな感じだった。

前処理();
DBにテストデータ投入(setupFilePath);
ret = テスト対象の実行();
DBデータのダンプ (expectFilePath); // ここは不要ならコメントアウト or 削除する
レスポンスの検証(ret);
DBの比較検証(expectFilePath); // 「DBデータのダンプ 」がコメントアウトされていなければ絶対に通る
後処理();

というような感じで1テストメソッド毎に書いていた。実際は細かい事をいろいろやっていた(スタックトレースからテストのクラス名やメソッド名を抜き出してtest/resources/testClass/testmethod.xlsみたいなファイルパスを作るヘルパメソッドを作ったりしていた)

Excelで書けるのは便利でオートフィルで連番を作ったりできて簡単にそれっぽいテストデータが作れた。Null値はIDataSetにフィルタを書ける機能があったのでそれで表現したりした。量産も簡単で他のテストケースから前処理や検証をチョチョイと変えると無数にテストケースができる感じになった。テストでロジックが守られる安心感が得られたと思った。

しかし、問題はあった。

Excelでテストデータを管理すると差分があった時に何かわからずレビュー時はExcelを一々開く必要があった。Excelだと当時はSVNリポジトリの動機が結構しんどかった。また謎の空行が1レコードと認識されてINSERTに失敗することがあった(Excelの終端の行の闇)

DBのテストは非常に遅かった。数分ぐらいかかっていた記憶があるが、当時巷のテスト事情を見ると普通に数数十分かかっている話を聞いてびっくりしていたので、それに比べるとうちの規模は大したこと無かったのかもしれない。十分早い方だったかもしれないが・・・)。 確かCLEAN-INSERTのコストに時間が結構かかっていたか何かだった。前のテスト結果のデータの量によっては結構な数を削除したりしていたので時間がかかっていたような気がする。そこで前処理でトランザクション開始とデータ投入、後処理でロールバックをすると少し早くなった。

ただ、テスト失敗後にDBの状態が見れなくなってしまいデバッガを使わないと調査できなかったり、そもそもトランザクションを内部で扱う処理には相性が悪かったのでJDBCのConnection.commitメソッド無視する邪悪なラッパーを書いたり、テストDBの処理にトランザクションを使うかDELETE-INSERTを使うかを選べるようにした。DBテストのための謎の仕組みがどんどんできてしまい、私しかメンテナンスできなくなった時期があった。

まぁでも「JUnitのテストランナーをいじる - 日々量産」とか色々JUnitの仕組みを少し触れたりしていて学ぶ楽しさを感じていたのかも。

今は引き継いで離れたが、まだ動いているという話を聞いているので負の遺産になっていそうではある。すまん。

あと、カラムの仕様変更に非常に弱かった。1個1個Excelファイルを開いて直すのは非常につらかった(数百メソッドはあった)

後日、JJUGのTDDに関する発表でDBのテストの話があった。当時のレポートを書いていたので貼る。

ryozi.hatenadiary.jp

JJUG CCC 2014 fall 「私がTDD出来ないのはどう考えてもお前らが悪い!」~エンタープライズJava開発でのTDD適用の…

当時はもうだいぶ作り終えていて、DBUnitのデータをExcelでやってた時の後悔はXMLだったら軽減できたなと思い、次に生かそうと思っていた記憶がある。

2012/01とかの呟き。(過去のツイートはバックアップして消したのでバックアップから失礼)

追い込まれている感があるが、1か月1200以上ツイートしているのでまだ元気ですね(他人事)

カラム追加で既存のテストがぶっ壊れて苦しんでる様子。

2014~2015: Java SE 6 + Oracle DB+ JUnit4 + DBUnit

要約: DBのテストは前のプロジェクトと一緒で実物を用意してやってた。

色々な小さいプロジェクトを転々として気づいたら2015年になっていた。(導入検証とかソースコードが失われたApache moduleリバースエンジニアリングとかでDBテストあまりやってなかった)

ここでもJavaだった。前のベンダが設計だけしてうちに引き継いできたが、設計が実装のことを考えてない作りで色々炎上した案件。終電は当たり前。10時出社できず寝坊して上司に怒られる日々だった。仕様の確認と設計書の修正をしていたりなんか実装始まってないのに結合試験が始まったり、結合試験駆動開発みたいになってた。もちろんバグもいっぱい出た。そしてバグっていることは分かるがログからだと何が起きているかよくわからなかったり、ログをたくさん出すようにしたら結合試験中にDisk Fullでしんだり。何とかリリースしても連携元が変なデータを流し込んでくるのとこちらもはじく仕組みが無かったせいでDBの不整合が起きて毎日不整合を見抜いてデータパッチでごまかすとかやっていた。当時はSQLは苦手だったがこのおかげで簡単なJOINやCRUDなら抵抗なく書けるようになったので人間やればできるという気持ちになった。あと、SVNのブランチで複数人開発やマージ地獄(デスマージと呼んでた)で疲弊したりしていた。このころから開発サーバにJenkinsを入れて、Mavenでwarを作るようになった気がする。他にもいろいろあったけど、要するに大変でした。

で、本題のDBはやはり本物を用意するやり方だった。しかしOracle DBを各開発者マシンには用意できないので、検証環境にOracle DBのインスタンスを複数立てて、各自で割り振って使っていた。

私が過去に作ったDBUnitのユーティリティ群は使わず、既にプロジェクトリーダーが作ったDBUnitの作法に従った。私がやっていたようなトリッキーなことはせず全部スタティックメソッドにまとめていて私でも他の人でも読み解けた。しいて言えば、JJUGからの学びでデータはXMLで管理するように進言して、言い出しっぺの法則によりExcelのデータをXMLに変換したりした。

対話に失敗している様子。

2015年末からしばらくはまた別案件でWordpressをいじる(引継ぎ無し・開発環境無し)とかの仕事になり、DBのテストとは無縁となります

2017~: Python + Django + PostgreSQL

PoCで作ったアプリをAzure移行かねて全部任されたもの。もちろんテストはない。

実際のDBを使ったテストを書いたが、ちょっとどうやっていたかあまり覚えていない。思い出したら書きます

その後またいろいろあってDBのテストから離れます。

2021~: C# + EFCore + PostgreSQL

C#です。というのも、Azure WebApp, Functionsを使いたく色々な言語でやった結果、C#以外はまともに書けない気がしたため。

ここは新規で私が好き勝手やってよいということだった。クリーンアーキテクチャにちょっと影響されて、外部の依存箇所(DB, HTTP-API等)はインタフェース化して依存を.NETのDIコンテナで解決させるようにすることにした。DBはとりあえずRepository層という切り分けにして、各機能で必要なデータ操作をまとめた各機能Repositoryみたいなクラスを作っていった。 (名前空間の切り方は悩んでましたが、「層(Controller,View,Model)」のような分け方ではなく「機能」で分けてその中に必要な層を置くようにした。正しいかどうかは今も分かってない)

ちょっと後悔しているのはEFCoreを採用しているのにRepository層を作ってしまったこと。Repositoryで抽象化層を挟まないとEFCoreのDbContextを依存させることになり、DBと密結合な実装になってしまう。DBのバックエンドをSQLiteとかにする手もありますが、なんかなんだで同じクエリが使えないならあんまり意味がない。

一応EFCoreにRepository層を作る話は書かれているが・・・

Entity Framework Core でインフラストラクチャの永続レイヤーを実装する | Microsoft Learn

EFCoreのChangeTrackerとRepositoryは相性悪すぎませんかね・・・少なくともEFCoreで扱うEntityとRepositoryで扱うEntity(Model)は同じにしないとうまくいかない気がします(ここを分けてしまった)

EFCoreがよくわからないのに採用したのがよくなかったです。.NET 6対応を兼ねてアップデートしようとしたが、EFCore 3.xから6.xでLEFT JOINの挙動が変わって動かなくなったが少なくない箇所で使っているのでEFCoreだけアップグレードを諦めていたりしている。Dapperに移行できないか考えている。

ともかく、とりあえずはRepositoryの実装はDBに依存したテスト、Repositoryに依存した実装はRepositoryのモックを渡すことでテストできるようになった。ただ例外もあって、数十個のテーブルからデータをかき集めて色々処理するクソデカバッチが少しあり、ここだけではDB依存のテストになっていたりする。

後は先頭に書いた通り、これで正しいんだっけ?と思いながらコードを書いたりレビューしている。なんもわからん。