📝

TestContainersとPodmanでEFCoreを使った.NETアプリケーションの統合テストを実装する

に公開

はじめに

『単体テストの考え方/使い方』における統合テストの位置づけ

統合テストの重要性を語る上で、Vladimir Khorikov氏の著書 『単体テストの考え方・使い方』 は外せません。同書では、統合テストを「単体テストではないすべてのテスト」と定義した上で、特に「プロセス外依存」をどのように取り扱い、テストの質を高めていくかに焦点を当てています。

その中でも、データベースのように「そのアプリケーションからのみアクセスされる依存(管理下の依存)」については、そのアプリケーションの実装の詳細であり、モックせず実機を使用すべきであると主張されています。

プロセス外依存をすべてモックし、アプリケーションレイヤーへのテストを実装してしまうと、それはドメインモデルのロジックを呼び出すだけの、実行価値の薄い**「取るに足らない(Trivialな)テスト」**になってしまう。

依存を安易にモックに置き換えることは、テストの耐退行性(バグを検知する力)を著しく下げ、本来検出すべき不整合を見逃すリスクを孕んでいます。

EF Coreにおける統合テストの重要性

実際、EF Coreを採用したアプリケーションにおいても、モックでは防げない以下のような問題がしばしば発生します。

  • クエリはEF CoreがSQLに解釈できる形でLINQを書かないといけないという制約がある一方で、LINQ to Objectではビルドも通るし、DBアクセス部分をモックすればユニットテストも通る。一方でいざプロダクションコードを動かすと例外で落ちる
  • ビルドも通り、テストコードも問題ないが、マイグレーションが実行できない

このような事情があるため、アプリケーションの規模が大きくなってくると、上記の事情を加味して手動テストで細かくテストしたり、あるいは見逃されてそのままリリース、、、ということもかんがえられるわけです。

また、GitHub Copilot 等の生成 AI に実装を委ねる現代の開発スタイルにおいて、実際のDBへの依存を用いた統合テストを実行できる環境があれば、DB マイグレーションを含めた整合性を AI 自身が検証できるため、より人の手間を減らすという意味でも重要になってきます。

そこで、統合テストで用いるDBの依存をTestContainersとPodmanによって注入し、それをテストコードから利用することができないかということで試してみました。

統合テストの実装方針

DBへの依存をコンテナによって注入する

テスト環境の冪等性と CI/CD への適応しやすさ

共有DBや実際にOSにインストールされたDBを使用する方法もあるかと思いますが、テストの動作の不安定さを招く可能性があります。そこで、コンテナを採用し、テストプロセス内で動的に DBコンテナをセットアップし、実行後に破棄することで、比較的安定した依存にすることができます。

これにより、開発者のローカル環境や CI/CD パイプライン上において、常にクリーンかつ隔離された環境でのテスト実行が保証されるのが良い点です。

なぜ Testcontainers,Podman を選択するのか

Dockerの有償化

現状、従業員数 250 人以上、または年間収益が1000万ドルを超える企業でDocker Desktopを商用利用する場合は有償ライセンスが必要になります。
https://www.docker.com/ja-jp/products/personal/

EF CoreのInMemory プロバイダーをMS公式が非推奨としている

以下の通りMS公式が非推奨としています。

https://learn.microsoft.com/ja-jp/ef/core/providers/in-memory/?tabs=dotnet-core-cli

TestContainersによるコンテナアクセスの抽象化

Dockerにせよ、Podmanにせよ、コンテナによるテストを.NETで実現する場合
コンテナへのアクセスをどう実装するか?という課題がありますが、TestContainersが解決してくれます。また、本稿執筆時点でもMITライセンスであり、ビジネス利用において扱いやすい点も大きなメリットですね。

環境構築

Podmanを利用する環境準備を行います。
以下のURLから Podman Desktopのインストーラーを使用してインストールします。
インストーラーのセットアップの過程でPodman自体のインストールも含まれています。
https://podman-desktop.io/

続いて、テスト実行時に利用する環境変数を設定するため、runsettingsを編集します。

Podmanの場合、Docker互換のAPIがNamed Pipeでアクセスできるため、下記のコマンドを用いてパスを取得し、runsettingsとして設定します。

C:\Users> "npipe:" + ((podman machine inspect | ConvertFrom-Json).ConnectionInfo.PodmanPipe.Path -replace '\\', '/')
npipe://./pipe/podman-machine-default

https://github.com/Tatsuyo606/testcontainers-efcoredemo/blob/8420a32e29586fca7fe3058ceefea97476708cbf/tests/TodoApi.IntegrationTests/integrationtests.runsettings#L1-L9

実装解説

xUnitの場合、テストスイート全体でリソースを共有するために ICollectionFixture を活用できます。コンテナの起動は比較的重い処理ですが、Fixtureによってコンテナをクラス間で再利用することで、テスト実行のパフォーマンス低下を最小限に抑えつつ、堅牢な検証環境を構築できます。
https://github.com/Tatsuyo606/testcontainers-efcoredemo/blob/8420a32e29586fca7fe3058ceefea97476708cbf/tests/TodoApi.IntegrationTests/Infrastructure/PostgreSqlContainerFixture.cs#L1-L29

https://github.com/Tatsuyo606/testcontainers-efcoredemo/blob/8420a32e29586fca7fe3058ceefea97476708cbf/tests/TodoApi.IntegrationTests/Infrastructure/IntegrationTestCollection.cs#L1-L4

あとは、利用したいテストコード側で下記のように実装することで、実行時のコンストラクタ引数にFixtureのインスタンスを注入してくれます。また、テストが完了し、インスタンスを破棄するときに、テーブル内データを消去する実装も加えています。この部分はベースクラスにまとめるなど共通化してもよいと思います。

https://github.com/Tatsuyo606/testcontainers-efcoredemo/blob/8420a32e29586fca7fe3058ceefea97476708cbf/tests/TodoApi.IntegrationTests/Queries/GetAllTodosQueryHandlerTests.cs#L1-L48

実装の検証として、あえてLINQ to Entitiesで解釈不能なコードを混入させ、テストが期待通りに失敗(例外を捕捉)することを確認します。これにより、モックによる単体テストでは検知できない不整合を、統合テストが正しく捉えていることが実証されます。

https://github.com/Tatsuyo606/testcontainers-efcoredemo/blob/8420a32e29586fca7fe3058ceefea97476708cbf/tests/TodoApi.IntegrationTests/Queries/GetHighPriorityTodosQueryHandlerTests.cs#L1-L43

あとはテストを実行し、結果を確認します。正常に動作することを確認できました。


テストエクスプローラーでの実行結果

考察

統合テストは依存関係のArrangeなどのコストが高く、場合によってはテストを実装するのが大変では?

これに関してはドメインが本来持つべきロジックがアプリケーションに流出していないか?を意識し、アプリケーションの責務をドメインモデルやプロセス外依存との対話に専念させ、ドメインモデルへロジックを集めて単体テストでカバーするように設計を見直すべき、と考えられます。これはテストの実装しづらさが、設計の嫌な匂いを示してくれている、とも言えそうです。

まとめ

  • 統合テストを実装するうえで、プロセス外依存をどう扱うか?は重要な観点であり、特にDBに関しては管理下の依存にあることが多く、実際の依存を用いたテストが望ましいことがある。
  • EF Coreにおいても、統合テストを実装したくなるケースはある
  • 統合テストを扱ううえで必要なDBへの依存はPodman、TestContainersを用いて、不安定さを排除しながら解決することができる

参考文献

単体テストの考え方/使い方

Discussion