前回までは、EF Core を使ったデータアクセス層の作り方を見てきました。
今回からは、そのデータアクセス層をテストで確認します。
データアクセス層のコードは、単体テストだけでは確認しきれないことがあります。
なぜなら、LINQ が実際にどの SQL に変換されるか、外部キー制約がどう働くか、同時実行制御が本当に例外になるかは、データベースと組み合わせて初めて見えるからです。
そこで、データベースを使った統合テストを用意します。
何をテストしたいのか
データアクセス層のテストでは、次のようなことを確認します。
- 期待した条件でデータを取得できる
- 関連データを含めて読み込める
- 追加、更新、削除が保存される
- 外部キー制約や削除制約が働く
- 同時実行の競合を検出できる
- 履歴テーブルや生 SQL の問い合わせが動く
- リポジトリのメソッド名と実際の動作が一致している
テストの目的は、EF Core 自体をテストすることではありません。
自分たちが書いたマッピング、クエリ、保存処理、例外変換が、意図どおりに動くかを確認することです。
テストプロジェクトを作る
ここでは xUnit を使います。
dotnet new xunit -n AutoLot.Dal.Tests
dotnet add AutoLot.Dal.Tests reference AutoLot.Dal
dotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore.SqlServer
dotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore.Design
テストプロジェクトは、データアクセス層のプロジェクトを参照します。
アプリケーション本体の画面プロジェクトには依存しないようにしておくと、テストの焦点がぶれません。
構成の例です。
AutoLot.Models
AutoLot.Dal
AutoLot.Dal.Tests
AutoLot.Web
AutoLot.Dal.Tests は AutoLot.Dal を参照します。
AutoLot.Dal は AutoLot.Models を参照します。
内部クラスをテストから見えるようにする
データアクセス層の中に internal な型を置いている場合、テストプロジェクトから見えません。
その場合は、データアクセス層のプロジェクトに次のような属性を追加できます。
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AutoLot.Dal.Tests")]
ファイル名は AssemblyInfo.cs でも構いません。
ただし、何でも internal のままテストから触るべきという意味ではありません。
公開 API として確認したいものは public にし、実装詳細は必要な範囲だけ見えるようにします。
テストしやすさのために設計を壊すのではなく、設計の境界を意識しながら必要な穴を開ける、という感覚です。
共通 using を用意する
テストでは、同じ名前空間を何度も使います。
GlobalUsings.cs にまとめておくと、各テストファイルが読みやすくなります。
global using AutoLot.Dal;
global using AutoLot.Dal.Repositories;
global using AutoLot.Models.Entities;
global using AutoLot.Models.ViewModels;
global using Microsoft.EntityFrameworkCore;
global using Xunit;
便利ですが、増やしすぎには注意します。
テストで頻繁に使うものだけに絞ると、型の出どころを見失いにくくなります。
テスト用の接続設定
テストでは、本番や開発用のデータベースとは別のデータベースを使います。
{
"ConnectionStrings": {
"AutoLotTest": "Server=(localdb)\\mssqllocaldb;Database=AutoLot_Test;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
学習用ならコードに直接書いても動きますが、実務では設定ファイルや環境変数から読み込む方が安全です。
テスト用 DbContextOptions を作るヘルパーです。
public static class TestDbContextOptions
{
public static DbContextOptions<AutoLotContext> Create()
{
return new DbContextOptionsBuilder<AutoLotContext>()
.UseSqlServer(
"Server=(localdb)\\mssqllocaldb;Database=AutoLot_Test;Trusted_Connection=True;TrustServerCertificate=True;")
.EnableSensitiveDataLogging()
.Options;
}
}
EnableSensitiveDataLogging() は、失敗時に値を含むログを出して原因を追いやすくします。
ただし、個人情報や機密情報を含む環境では慎重に使います。
テスト用のデータベース初期化
毎回同じ状態からテストを始めるために、初期化処理を用意します。
public sealed class TestDatabaseInitializer
{
private readonly AutoLotContext _context;
public TestDatabaseInitializer(AutoLotContext context)
{
_context = context;
}
public async Task ResetAsync()
{
await _context.Database.EnsureDeletedAsync();
await _context.Database.MigrateAsync();
await SeedAsync();
}
private async Task SeedAsync()
{
var cars = new List<Car>
{
new("Toyota", "Blue", "Aqua"),
new("Honda", "White", "Snow"),
new("Ford", "Black", "Night")
};
cars[0].Orders.Add(new Order
{
CustomerName = "佐藤",
OrderDate = new DateTime(2026, 8, 14)
});
await _context.Cars.AddRangeAsync(cars);
await _context.SaveChangesAsync();
}
}
EnsureCreatedAsync() ではなく MigrateAsync() を使っているのは、マイグレーションの動作も含めて確認したいからです。
テストで速度を優先したい場合は、別の方法を選ぶこともあります。
ただ、データアクセス層の統合テストでは、実際のスキーマに近い状態で動かす価値があります。
共通基底クラスを作る
テストごとに DbContext を作る処理を書くと重複します。
共通基底クラスにまとめます。
public abstract class DatabaseTestBase : IAsyncLifetime
{
protected AutoLotContext Context { get; private set; } = default!;
public async Task InitializeAsync()
{
Context = new AutoLotContext(TestDbContextOptions.Create());
var initializer = new TestDatabaseInitializer(Context);
await initializer.ResetAsync();
}
public async Task DisposeAsync()
{
await Context.DisposeAsync();
}
}
テストクラスは、この基底クラスを継承します。
public sealed class SmokeTests : DatabaseTestBase
{
[Fact]
public async Task データベースに接続できる()
{
var canConnect = await Context.Database.CanConnectAsync();
Assert.True(canConnect);
}
}
まずは接続できることを確認します。
地味ですが、このテストが通ると、接続文字列、データベース作成、マイグレーション適用の土台が動いているとわかります。
テストデータは意味のある名前にする
テストデータは、単なるダミーではありません。
テストの読みやすさを支える資料でもあります。
new("Toyota", "Blue", "Aqua")
new("Honda", "White", "Snow")
new("Ford", "Black", "Night")
このように、検索条件や期待値を読み取れる名前にしておくと、テストが失敗したときに原因を追いやすくなります。
逆に、すべて Test1、Test2、Test3 のような名前だと、何を確認しているのかが見えにくくなります。
テストの独立性を保つ
統合テストでは、あるテストの変更が別のテストに影響しないようにすることが重要です。
今回の基底クラスでは、テストごとにデータベースを削除し、マイグレーションを適用し、初期データを入れ直しています。
これは確実ですが、件数が増えると遅くなります。
実務では、次のような方法も使われます。
- テストクラスごとに初期化する
- トランザクションを使ってテスト後に戻す
- コンテナ上のデータベースを使い捨てる
- テストごとにデータベース名を変える
- 読み取りテストと更新テストで初期化方法を分ける
最初は確実な方法を選び、遅くなってから改善する方が安全です。
この記事のまとめ
データアクセス層のテストでは、テストプロジェクト、参照設定、DbContextOptions、データベース初期化、共通基底クラスを用意します。
ここが整うと、以降のテストはかなり書きやすくなります。
次回は、この土台を使って、問い合わせ処理のテストを書いていきます。