dotnet-testing-strategy
Deciding how to test .NET code. Unit vs integration vs E2E decision tree, test doubles.
What this skill does
# dotnet-testing-strategy
Decision framework for choosing the right test type, organizing test projects, and selecting test doubles in .NET applications. Covers unit vs integration vs E2E trade-offs with concrete criteria, naming conventions, and when to use mocks vs fakes vs stubs.
**Out of scope:** Test project scaffolding (directory layout, xUnit project creation, coverlet setup, editorconfig overrides) is owned by [skill:dotnet-add-testing]. Code coverage tooling and mutation testing are covered by [skill:dotnet-test-quality]. CI test reporting and pipeline integration -- see [skill:dotnet-gha-build-test] and [skill:dotnet-ado-build-test].
**Prerequisites:** Run [skill:dotnet-project-analysis] to understand the solution structure before designing a test strategy.
Cross-references: [skill:dotnet-xunit] for xUnit v3 testing framework features, [skill:dotnet-integration-testing] for WebApplicationFactory and Testcontainers patterns, [skill:dotnet-snapshot-testing] for Verify-based approval testing, [skill:dotnet-test-quality] for coverage and mutation testing, [skill:dotnet-add-testing] for test project scaffolding.
---
## Test Type Decision Tree
Use this decision tree to determine which test type fits a given scenario. Start at the top and follow the first matching criterion.
```
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)
```
### Concrete Criteria by Test Type
| Test Type | Infrastructure | Speed | Scope | When to Use |
|-----------|---------------|-------|-------|-------------|
| **Unit** | None (mocked/faked) | <10ms per test | Single class/method | Pure logic, domain rules, value objects, transformations, validators |
| **Integration** | Real (DB, HTTP) | 100ms-5s per test | Multiple components | Repository queries, API contract verification, serialization round-trips, middleware behavior |
| **E2E / Functional** | Full stack | 1-30s per test | Entire request pipeline | Critical user flows, auth + routing + middleware combined, cross-cutting concern verification |
### Cost-Benefit Guidance
- **Prefer unit tests** for business logic. They run fast, pinpoint failures precisely, and have no infrastructure requirements.
- **Use integration tests** to verify infrastructure boundaries work correctly. A repository unit test with a mocked `DbContext` proves nothing about actual SQL generation -- use a real database via Testcontainers.
- **Use E2E tests sparingly** for critical paths only. They are slow, brittle, and expensive to maintain. Cover the happy path and one or two critical failure scenarios.
- **The testing pyramid is a guideline, not a rule.** Some applications (CRUD APIs with minimal logic) benefit from more integration tests than unit tests. Match the strategy to the application's complexity profile.
---
## Test Organization
### Project Naming Convention
Mirror the `src/` project structure under `tests/` with a suffix indicating test type:
```
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` -- isolated tests, no external dependencies
- `*.IntegrationTests` -- real infrastructure (database, HTTP, file system)
- `*.FunctionalTests` -- full application stack via `WebApplicationFactory`
See [skill:dotnet-add-testing] for creating these projects with proper package references and build configuration.
### Test Class Organization
One test class per production class. Place test files in a namespace that mirrors the production namespace:
```csharp
// 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
}
```
For large production classes, split test classes by method:
```csharp
// OrderService_CreateTests.cs
// OrderService_CancelTests.cs
// OrderService_RefundTests.cs
```
---
## Test Naming Conventions
Use the `Method_Scenario_ExpectedBehavior` pattern. This reads naturally in test explorer output and makes failures self-documenting:
```csharp
public class OrderServiceTests
{
[Fact]
public void CalculateTotal_WithDiscountCode_AppliesPercentageDiscount()
{
// ...
}
[Fact]
public void CalculateTotal_WithExpiredDiscount_ThrowsInvalidOperationException()
{
// ...
}
[Fact]
public async Task SubmitOrder_WhenInventoryInsufficient_ReturnsOutOfStockError()
{
// ...
}
}
```
Alternative naming styles (choose one per project and stay consistent):
| Style | Example |
|-------|---------|
| `Method_Scenario_Expected` | `CalculateTotal_EmptyCart_ReturnsZero` |
| `Should_Expected_When_Scenario` | `Should_ReturnZero_When_CartIsEmpty` |
| `Given_When_Then` | `GivenEmptyCart_WhenCalculatingTotal_ThenReturnsZero` |
---
## Arrange-Act-Assert Pattern
Every test follows the AAA structure. Keep each section clearly separated:
```csharp
[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));
}
```
**Guideline:** If you cannot clearly label the three sections, the test may be doing too much. Split into multiple tests.
---
## Test Doubles: When to Use What
### Terminology
| Double Type | Behavior | State Verification | Use When |
|-------------|----------|-------------------|----------|
| **Stub** | Returns canned data | No | You need a dependency to return specific values so the code under test can proceed |
| **Mock** | Verifies interactions | Yes (interaction) | You need to verify that the code under test called a dependency in a specific way |
| **Fake** | Working implementation | Yes (state) | You need a lightweight but functional substitute (in-memory repository, in-memory message bus) |
| **Spy** | Records calls for later assertion | Yes (interaction) | You need to verify calls happened without prescribing them upfront |
### Decision Guidance
```
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 valRelated in Code Review
gstack
IncludedFast headless browser for QA testing and site dogfooding. Navigate pages, interact with elements, verify state, diff before/after, take annotated screenshots, test responsive layouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or test a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)
startup-due-diligence
IncludedLegal due diligence review for seed-stage and Series A startups (US, Delaware C-Corp focus). Supports both investor and founder perspectives. Capabilities include: (1) Interactive document review and issue spotting; (2) Document request list generation; (3) Cap table and SAFE/convertible note analysis; (4) Red flag identification with severity ratings; (5) Diligence report generation. TRIGGERS: due diligence, DD, startup investment, cap table review, Series A, seed round, investor diligence, legal review startup, SAFE analysis, convertible note, 409A, founder vesting.
interview-master
IncludedThis skill should be used when the user asks to "generate interview questions", "prepare for interview", "optimize resume", "conduct mock interview", "analyze git commits for resume", "generate resume from code", "review my resume", or mentions interview preparation, career assistance, or extracting project experience from git history. Provides comprehensive interview and career development guidance for both job seekers and interviewers.
fix-issue
IncludedFixes GitHub issues using parallel analysis agents for root cause investigation, code exploration, and regression detection. Reads issue context from gh CLI, searches codebase and memory for related patterns, generates a fix with tests, and links the resolution back to the issue via PR. Includes prevention analysis to avoid recurrence. Use when debugging errors, resolving regressions, fixing bugs, or triaging issues.
sf-apex
IncludedGenerates and reviews Salesforce Apex code with 150-point scoring. TRIGGER when: user writes, reviews, or fixes Apex classes, triggers, test classes, batch/queueable/schedulable jobs, or touches .cls/.trigger files. DO NOT TRIGGER when: LWC JavaScript (use sf-lwc), Flow XML (use sf-flow), SOQL-only queries (use sf-soql), or non-Salesforce code.
swift-development
IncludedComprehensive Swift development for building, testing, and deploying iOS/macOS applications. Use when Claude needs to: (1) Build Swift packages or Xcode projects from command line, (2) Run tests with XCTest or Swift Testing framework, (3) Manage iOS simulators with simctl, (4) Handle code signing, provisioning profiles, and app distribution, (5) Format or lint Swift code with SwiftFormat/SwiftLint, (6) Work with Swift Package Manager (SPM), (7) Implement Swift 6 concurrency patterns (async/await, actors, Sendable), (8) Create SwiftUI views with MVVM architecture, (9) Set up Core Data or SwiftData persistence, or any other Swift/iOS/macOS development tasks.