第30章 テスト② Queryは統合寄り(現実路線)🧫✨
(=DBと一緒に動かして確かめるのが、いちばん事故が減るよ〜!って話です😊)
この章のゴール🎯💖
この章が終わったら、あなたはこうなれます👇✨
- Query(一覧・検索・集計)が壊れてないかを、DB込みでチェックできる🧪
- 「Queryを単体テストで頑張りすぎて沼る😵」を回避できる
- **スモークテスト(最低限の安心)**を短時間で作れる🔥
なんでQueryは“統合テスト寄り”なの?🤔🧠

Queryって、実体はだいたいこう👇
- LINQ / SQL(DBに投げるやつ)
- JOIN・集計・フィルタ・ソート・ページング
- DTO投影(Selectで形を作る)
で、ここがポイント💥 LINQは「C#で書ける」けど、最後はDBのSQLに変換されるので、 単体テストでモックしても「本番で動かない」事故が起きやすいんです😇
Microsoftの統合テストの公式ガイドでも、統合テストはDBなどのインフラ込みで動かす前提で説明されていて、EF Coreのテスト用DBについても注意があります。(Microsoft Learn)
まず結論:Queryのテストはこの3段階でOK💡🧪
おすすめはこの順番✨(全部やらなくてOK!)
- ハンドラ統合テスト(DB + QueryHandler だけ)🧪
- API統合テスト(HTTPで叩く:スモーク向き)🌐
- 本番と同じDBで統合(Testcontainersなど:余裕が出たら)🐳🔥
大事な注意:EF Coreの“InMemoryプロバイダ”は万能じゃない⚠️🧊
「楽そうだから InMemory 使お〜」ってやりがちなんだけど… 公式でも InMemoryは“限定的”、インメモリでやるなら SQLite推奨って流れです。(Microsoft Learn)
さらにEF Core公式のテスト解説でも、DbSetをモックしてQueryをテストするのは難しくて非推奨寄り、SQLite in-memoryの方が互換性が高い(ただし差は残る)と説明されています。(Microsoft Learn)
この章では SQLite in-memory を軸にいきます😊🫶
1️⃣ ハンドラ統合テスト(DB + QueryHandler)🧪✨
ねらい🎯
- SQL変換ミス(Whereが効いてない、Joinで死ぬ、ページング変など)を早めに検知する
- テストは速くしたいので、Webサーバーは立てない(ここ重要)⚡
手順A:SQLite in-memory を“正しく”使うコツ🔑🧠
SQLiteのin-memoryは 接続(Connection)が閉じるとDBも消えるので、 テスト中は 接続を開きっぱなしにします🚪🔓
例:QueryHandlerの統合テスト(テンプレ)🧪🧸
ここでは題材として「ToDo一覧Query」を想定して書くね😊 あなたのプロジェクトの
GetXxxListQueryHandlerに読み替えればOK!
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Xunit;
public sealed class GetTodoListQueryIntegrationTests
{
[Fact]
public async Task Keywordで絞り込みできる()
{
// 1) SQLite in-memory: 接続を開きっぱなしにする
await using var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();
// 2) DbContext を SQLite に向ける
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
// 3) スキーマ作成 & Seed
await using (var setup = new AppDbContext(options))
{
await setup.Database.EnsureCreatedAsync();
setup.TodoItems.AddRange(
new TodoItem { Title = "牛乳を買う", IsDone = false },
new TodoItem { Title = "レポート提出", IsDone = true },
new TodoItem { Title = "牛乳プリン作る", IsDone = false }
);
await setup.SaveChangesAsync();
}
// 4) テスト対象を実行(QueryHandler)
await using (var db = new AppDbContext(options))
{
var handler = new GetTodoListQueryHandler(db);
var result = await handler.Handle(new GetTodoListQuery(
Keyword: "牛乳",
Page: 1,
PageSize: 10
));
// 5) Assert(期待どおり絞り込めてる?)
Assert.Equal(2, result.Items.Count);
Assert.All(result.Items, x => Assert.Contains("牛乳", x.Title));
}
}
}
ここ、テストとしてめちゃ大事💖
- Seedは最小でOK(3件とかで十分)🌱
- Assertは「条件が効いてる」ことに絞る(全部チェックしない)🧪✨
よくあるつまずき😵💫🌀(そして即解決)
❌「no such table…」になる
✅ 原因:SQLite in-memory が消えた(接続が閉じた) ✅ 対策:接続をOpenしたままテスト全体で使う(上の例の通り)🔓
❌ テストがたまに落ちる(順序が違う)
✅ 原因:ORDER BY なしで “たまたま順” に依存してる
✅ 対策:Query側で必ず .OrderBy(...) を入れる📌✨
❌ InMemoryだと通るのに本番で死ぬ
✅ あるある…😇 SQLite(または本番DB)で回すと、SQL変換の違いが露出します → だからこの章の方針が強い💪(Microsoft Learn)
2️⃣ API統合テスト(スモークテスト向き)🌐🧪
ねらい🎯
- ルーティング、DI、シリアライズ、フィルタ、例外処理など **“APIとして動くか”**をまとめて確認する✨
- Microsoftも
WebApplicationFactoryを使った統合テストを公式で案内しています。(NuGet)
最小の型:Program をテストから参照できるようにする🧩
Minimal APIでもControllerでも、これを Program.cs のどこかに置くのが定番です👇
public partial class Program { }
WebApplicationFactoryでDBをSQLite in-memoryに差し替える(雛形)🧪
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
public sealed class ApiFactory : WebApplicationFactory<Program>
{
private SqliteConnection? _connection;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// 既存のDbContext登録を消す
services.RemoveAll<DbContextOptions<AppDbContext>>();
// SQLite in-memory: 接続を開きっぱなし
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
services.AddDbContext<AppDbContext>(opt => opt.UseSqlite(_connection));
// スキーマ作成 & Seed
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
db.TodoItems.Add(new TodoItem { Title = "スモーク用", IsDone = false });
db.SaveChanges();
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing) _connection?.Dispose();
}
}
APIスモークテスト例(GET叩いてOKなら勝ち🎉)
using System.Net;
using Xunit;
public sealed class TodoApiSmokeTests : IClassFixture<ApiFactory>
{
private readonly ApiFactory _factory;
public TodoApiSmokeTests(ApiFactory factory) => _factory = factory;
[Fact]
public async Task Todo一覧が200で返る()
{
var client = _factory.CreateClient();
var res = await client.GetAsync("/api/todos?page=1&pageSize=10");
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
var json = await res.Content.ReadAsStringAsync();
Assert.Contains("スモーク用", json);
}
}
xUnit v3の小ネタ(困ったらここ疑って🥲)
最近は xUnit v3 も使えるけど、.NET 10 + WebApplicationFactory 周りでエラー報告が出てたりします。(Stack Overflow)
もしハマったら、まずはテスト基盤を xUnit v2 系に寄せると安定しやすいです🙏(ここは“完成させるのが最優先”でOK!)
3️⃣ もう一段リアルに:本番と同じDBで統合(余裕が出たら)🐳🔥

SQLiteでもかなり事故は減るけど、本番がSQL Server/PostgreSQLなら、 「本番DBでしか起きない癖」もあります😇
そこで便利なのが Testcontainers:テスト中だけDockerで本物DBを立てるやつ💪 SQL Serverを立てるモジュールも公式で案内されています。(dotnet.testcontainers.org)
そしてDBの掃除(初期化)に Respawn を組み合わせると、 テストが速くて安定しやすいです🧹✨(GitHub)
(この章では“紹介だけ”にして、やるなら発展編でOK😊)
4️⃣ ミニ演習(この章のゴールチェック✅🎀)
演習1:QueryHandler統合テストを1本作る🧪
- フィルタ(KeywordやStatus)を1個入れる
- 「絞り込みできてる?」だけAssertする
演習2:APIスモークテストを1本作る🌐
- GETを叩いて 200 OK
- JSONにSeedした文字が入ってたら勝ち🎉
5️⃣ AIに手伝ってもらうプロンプト例🤖💬✨
- 「このQuery(LINQ)に対して、落ちやすい境界条件のテストケースを10個出して」
- 「SQLite in-memoryでEF Core統合テストを書くxUnitテンプレを作って(接続は開きっぱなしで)」
- 「このAPIのスモークテストを
WebApplicationFactoryで最小構成にして」
まとめ🌸
Queryは“DBと一緒に”動かしてこそ安心🥹🫶 この章のスタイル(SQLite in-memory統合 + APIスモーク)を持っておくと、 CQRSのRead側が一気に「壊れにくい」チームになります🎉✨
次に進むなら、Queryの性能(計測→改善)に入っていく流れが気持ちいいよ〜📏⚡