dotnet-testing-strategy について
dotnet-testing-strategyは、.NETチームのテスト方針策定を支援するスキルです。インフラ、リスク、保守コストを踏まえたテスト種別の選択、プロジェクト構成、モックやフェイクなどの使い分けを示します。
主な機能
- 01単体・統合・E2Eテストの選択
- 02テストプロジェクトとクラスの構成
- 03テスト命名とAAA形式の適用
- 04モック、フェイク、スタブ、スパイの区別
- 05よくあるテストアンチパターンの把握
活用シーン
- 01ビジネスロジックに適したテスト種別の選択
- 02本番プロジェクト構成に沿ったテスト整理
- 03依存関係に適したテストダブルの選定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-testing-strategyインストール後、Claude Code で/dotnet-testing-strategyを入力して読み込まれたか確認します。
詳細
dotnet-testing-strategy
.NET アプリケーションで適切なテストの種類を選び、テストプロジェクトを構成し、テストダブルを選択するための判断フレームワークです。具体的な基準、命名規則、モック・フェイク・スタブを使い分ける場面を含め、ユニットテスト、統合テスト、E2E テストのトレードオフを解説します。
対象外: テストプロジェクトのスキャフォールディング(ディレクトリ構成、xUnit プロジェクトの作成、coverlet のセットアップ、editorconfig のオーバーライド)は [skill:dotnet-add-testing] の対象です。コードカバレッジツールとミューテーションテストについては [skill:dotnet-test-quality] で扱います。CI のテストレポートとパイプライン統合については、[skill:dotnet-gha-build-test] と [skill:dotnet-ado-build-test] を参照してください。
前提条件: テスト戦略を設計する前に、[skill:dotnet-project-analysis] を実行してソリューションの構成を把握してください。
相互参照:[skill:dotnet-xunit] では xUnit v3 のテストフレームワーク機能、[skill:dotnet-integration-testing] では WebApplicationFactory と Testcontainers のパターン、[skill:dotnet-snapshot-testing] では Verify ベースの承認テスト、[skill:dotnet-test-quality] ではカバレッジとミューテーションテスト、[skill:dotnet-add-testing] ではテストプロジェクトのスキャフォールディングについて説明しています。
テストの種類を選ぶ判断ツリー
次の判断ツリーを使って、シナリオに適したテストの種類を判断してください。最上部から始め、最初に該当する基準に従ってください。
Does the code under test depend on external infrastructure?
(database, HTTP service, file system, message broker)
|
+-- YES --> Is the infrastructure behavior critical to correctness?
| |
| +-- YES --> Does it need the full application stack (middleware, auth, routing)?
| | |
| | +-- YES --> E2E / Functional Test
| | | (WebApplicationFactory or Playwright)
| | |
| | +-- NO --> Integration Test
| | (WebApplicationFactory or Testcontainers)
| |
| +-- NO --> Unit Test with test doubles
| (mock the infrastructure boundary)
|
+-- NO --> Is this pure logic (calculations, transformations, validation)?
|
+-- YES --> Unit Test (no test doubles needed)
|
+-- NO --> Unit Test with test doubles
(mock collaborator interfaces)
テストの種類ごとの具体的な基準
| テストの種類 | インフラストラクチャ | 速度 | 範囲 | 使用場面 |
|---|---|---|---|---|
| ユニット | なし(モック/フェイクを使用) | テストあたり <10ms | 単一のクラス/メソッド | 純粋なロジック、ドメインルール、値オブジェクト、変換、バリデーター |
| 統合 | 実環境(DB、HTTP) | テストあたり 100ms~5s | 複数のコンポーネント | リポジトリクエリ、API コントラクトの検証、シリアル化の往復、ミドルウェアの動作 |
| E2E/機能 | フルスタック | テストあたり 1~30s | リクエストパイプライン全体 | 重要なユーザーフロー、認証+ルーティング+ミドルウェアの組み合わせ、横断的関心事の検証 |
費用対効果の指針
- ビジネスロジックにはユニットテストを優先してください。 実行が速く、失敗箇所を正確に特定でき、インフラストラクチャも必要ありません。
- インフラストラクチャとの境界が正しく動作することの検証には、統合テストを使ってください。
DbContextをモックしたリポジトリのユニットテストでは、実際の SQL 生成について何も証明できません。Testcontainers を使って実データベースでテストしてください。 - E2E テストは重要な経路に限って使ってください。 実行が遅く、壊れやすく、保守コストも高くなります。正常系と、重要な失敗シナリオを 1~2 件カバーしてください。
- テストピラミッドは指針であり、規則ではありません。 ロジックが最小限の CRUD API など、一部のアプリケーションでは、ユニットテストより統合テストを多くする方が効果的です。アプリケーションの複雑さに応じて戦略を調整してください。
テストの構成
プロジェクトの命名規則
src/ のプロジェクト構成を tests/ 以下に反映し、テストの種類を示すサフィックスを付けてください。
MyApp/
src/
MyApp.Domain/
MyApp.Application/
MyApp.Api/
MyApp.Infrastructure/
tests/
MyApp.Domain.UnitTests/
MyApp.Application.UnitTests/
MyApp.Api.IntegrationTests/
MyApp.Api.FunctionalTests/
MyApp.Infrastructure.IntegrationTests/
*.UnitTests— 外部依存関係のない、分離されたテスト*.IntegrationTests— 実際のインフラストラクチャ(データベース、HTTP、ファイルシステム)を使うテスト*.FunctionalTests—WebApplicationFactoryを使ったアプリケーションスタック全体のテスト
適切なパッケージ参照とビルド構成でこれらのプロジェクトを作成する方法については、[skill:dotnet-add-testing] を参照してください。
テストクラスの構成
本番コードのクラスごとにテストクラスを 1 つ作成してください。テストファイルは本番コードの名前空間を反映した名前空間に配置します。
// Production: src/MyApp.Domain/Orders/OrderService.cs
// Test: tests/MyApp.Domain.UnitTests/Orders/OrderServiceTests.cs
namespace MyApp.Domain.UnitTests.Orders;
public class OrderServiceTests
{
// Group by method, then by scenario
}
本番コードのクラスが大きい場合は、メソッドごとにテストクラスを分割してください。
// OrderService_CreateTests.cs
// OrderService_CancelTests.cs
// OrderService_RefundTests.cs
テストの命名規則
Method_Scenario_ExpectedBehavior パターンを使ってください。テストエクスプローラーの出力で自然に読め、失敗の内容を見ただけで理解できます。
public class OrderServiceTests
{
[Fact]
public void CalculateTotal_WithDiscountCode_AppliesPercentageDiscount()
{
// ...
}
[Fact]
public void CalculateTotal_WithExpiredDiscount_ThrowsInvalidOperationException()
{
// ...
}
[Fact]
public async Task SubmitOrder_WhenInventoryInsufficient_ReturnsOutOfStockError()
{
// ...
}
}
別の命名スタイルを使う場合は、プロジェクトごとに 1 つ選び、一貫して使用してください。
| スタイル | 例 |
|---|---|
Method_Scenario_Expected | CalculateTotal_EmptyCart_ReturnsZero |
Should_Expected_When_Scenario | Should_ReturnZero_When_CartIsEmpty |
Given_When_Then | GivenEmptyCart_WhenCalculatingTotal_ThenReturnsZero |
Arrange-Act-Assert パターン
すべてのテストは AAA 構造に従います。各セクションを明確に分けてください。
[Fact]
public async Task CreateOrder_WithValidItems_PersistsAndReturnsOrder()
{
// Arrange
var repository = new FakeOrderRepository();
var service = new OrderService(repository);
var request = new CreateOrderRequest
{
CustomerId = "cust-123",
Items = [new OrderItem("SKU-001", Quantity: 2, UnitPrice: 29.99m)]
};
// Act
var result = await service.CreateAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal("cust-123", result.CustomerId);
Assert.Single(result.Items);
Assert.True(repository.SavedOrders.ContainsKey(result.Id));
}
ガイドライン: 3 つのセクションを明確に区別できない場合、そのテストは多くのことをしすぎている可能性があります。複数のテストに分割してください。
テストダブル:使い分け
用語
| ダブルの種類 | 動作 | 状態の検証 | 使用する場面 |
|---|---|---|---|
| Stub | 用意されたデータを返します | いいえ | テスト対象のコードが処理を続けられるよう、依存先に特定の値を返させる必要がある場合 |
| Mock | やり取りを検証します | はい(やり取り) | テスト対象のコードが依存先を特定の方法で呼び出したことを検証する必要がある場合 |
| Fake | 動作する実装です | はい(状態) | 軽量ながら機能する代替物(インメモリリポジトリ、インメモリメッセージバス)が必要な場合 |
| Spy | 後でアサーションするために呼び出しを記録します | はい(やり取り) | 事前に呼び出し方を指定せずに、呼び出しが行われたことを検証する必要がある場合 |
選択の指針
Do you need to verify HOW a dependency was called?
|
+-- YES --> Do you need a working implementation too?
| |
| +-- YES --> Spy (record calls on a fake)
| +-- NO --> Mock (NSubstitute / Moq)
|
+-- NO --> Do you need the dependency to DO something realistic?
|
+-- YES --> Fake (in-memory implementation)
+-- NO --> Stub (return canned values)
例:Stub、Mock、Fake の比較
// STUB: Returns canned data -- verifying the code under test's logic
var priceService = Substitute.For<IPriceService>();
priceService.GetPriceAsync("SKU-001").Returns(29.99m); // canned return
var total = await calculator.CalculateTotalAsync(items);
Assert.Equal(59.98m, total); // assert on the result, not the call
// MOCK: Verifies interaction -- ensuring a side effect happened
var emailSender = Substitute.For<IEmailSender>();
await orderService.CompleteAsync(order);
await emailSender.Received(1).SendAsync( // assert on the call
Arg.Is<string>(to => to == order.CustomerEmail),
Arg.Any<string>(),
Arg.Any<string>());
// FAKE: In-memory implementation -- realistic behavior without infrastructure
public class FakeOrderRepository : IOrderRepository
{
public Dictionary<Guid, Order> Orders { get; } = new();
public Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
=> Task.FromResult(Orders.GetValueOrDefault(id));
public Task SaveAsync(Order order, CancellationToken ct = default)
{
Orders[order.Id] = order;
return Task.CompletedTask;
}
}
Mock より Fake を優先する場合
- ドメインの要素が多いアプリケーション: Fake を使うと、複雑なやり取りをより現実的に再現できます。インメモリリポジトリは、Mock では見逃すバグ(重複キー違反など)を検出できます。
- Mock の多用はテストの問題の兆候です。 アサーションより Mock のセットアップのほうが多いテストでは、Fake を使うほうが明確で保守しやすいかどうかを検討してください。
- 統合境界は、Mock ではなく [skill:dotnet-integration-testing] を使って実際のインフラストラクチャでテストするほうが適しています。 Mock 化した
DbContextでは、LINQ が有効な SQL に変換されることを検証できません。
テストのアンチパターン
1. 実装の詳細をテストする
// BAD: Breaks when refactoring internals
repository.Received(1).GetByIdAsync(Arg.Is<Guid>(id => id == orderId));
repository.Received(1).SaveAsync(Arg.Any<Order>());
// ... five more Received() calls verifying the exact call sequence
// GOOD: Test the observable outcome
var result = await service.ProcessAsync(orderId);
Assert.Equal(OrderStatus.Completed, result.Status);
2. Mock の過剰なセットアップ
// BAD: Mock setup is longer than the actual test
var repo = Substitute.For<IOrderRepository>();
var pricing = Substitute.For<IPricingService>();
var inventory = Substitute.For<IInventoryService>();
var shipping = Substitute.For<IShippingService>();
var notification = Substitute.For<INotificationService>();
var audit = Substitute.For<IAuditService>();
// ... 20 lines of .Returns() setup
// BETTER: Use a builder or fake that encapsulates setup
var fixture = new OrderServiceFixture()
.WithOrder(testOrder)
.WithPrice("SKU-001", 29.99m);
var result = await fixture.Service.ProcessAsync(testOrder.Id);
3. 非決定的なテスト
テストはシステムクロック、ランダム値、外部ネットワークに依存してはいけません。抽象化を注入してください。
// BAD: Uses DateTime.UtcNow directly
public bool IsExpired() => ExpiresAt < DateTime.UtcNow;
// GOOD: Inject TimeProvider (.NET 8+)
public bool IsExpired(TimeProvider time) => ExpiresAt < time.GetUtcNow();
// In test
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 6, 15, 0, 0, 0, TimeSpan.Zero));
Assert.True(order.IsExpired(fakeTime));
重要な原則
- 実装ではなく動作をテストします。 内部メソッドの呼び出しではなく、観測可能な結果(戻り値、状態の変化、発行されたイベント)をアサートしてください。
- 1 つのテストにつき、論理的なアサーションは 1 つにします。 1 つの論理的な概念(返されたオブジェクトの全プロパティなど)を検証するのであれば、複数の
Assert呼び出しを使ってもかまいません。互いに関係のない複数のアサーションがある場合は、テストを分割してください。 - テストを独立させてください。 ほかのテストの実行や順序に依存するテストがあってはいけません。各テストで新しいフィクスチャを使ってください。
- 失敗の内容がテスト名だけで分かるようにしてください。 失敗したテストの名前を見れば、テスト本体を読まなくても何が壊れたか分かるようにしてください。
- リスクに応じてテストの種類を選んでください。 リスクの高いコード(決済、認証)には、統合テストと E2E テストでのカバレッジが必要です。リスクの低いコード(単純なマッピング)には、単体テストだけで十分です。
- 時刻に依存するロジックには
TimeProviderを使用してください(.NET 8 以降)。これはフレームワークが提供する抽象化です。独自のIClockインターフェースは作成しないでください。
エージェント向けの注意事項
- 自分が所有していない型を Mock 化しないでください。
HttpClient、DbContext、フレームワークの型を Mock 化すると、実際の動作を反映しない脆弱なテストになります。代わりにWebApplicationFactoryまたは Testcontainers を使用してください。詳しくは [skill:dotnet-integration-testing] を参照してください。 - 既存の構成を確認せずにテストプロジェクトを作成しないでください。 まず [skill:dotnet-project-analysis] を実行してください。テストインフラストラクチャが重複すると、ビルドの競合が発生します。
- テストで
Thread.Sleepを使用しないでください。 キャンセルトークンを指定してTask.Delayを使用するか、さらに適切な方法としてFakeTimeProvider.Advance()を使い、時間を決定論的に制御してください。 - プライベートメソッドを直接テストしないでください。 プライベートメソッドに独自のテストが必要なら、そのメソッドを独立したクラスに抽出してください。パブリック API を通じてテストしてください。
- 統合テストで接続文字列をハードコードしないでください。 破棄可能なインフラストラクチャには Testcontainers を、プロセス内でのテストには
WebApplicationFactoryを使用してください。詳しくは [skill:dotnet-integration-testing] を参照してください。