dotnet-testing-strategy

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-testing-strategy

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.

用于.NET应用中选择合适测试类型、组织测试项目以及选择测试替身的决策框架。涵盖单元测试、集成测试与E2E测试的权衡取舍,包含具体判断标准、命名规范,以及mock、fake和stub的适用场景。
超出范围: 测试项目脚手架(目录结构、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]了解解决方案结构。
交叉参考:xUnit v3测试框架特性请见[skill:dotnet-xunit],WebApplicationFactory和Testcontainers模式请见[skill:dotnet-integration-testing],基于Verify的验收测试请见[skill:dotnet-snapshot-testing],覆盖率和变异测试请见[skill:dotnet-test-quality],测试项目脚手架请见[skill:dotnet-add-testing]。

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)
使用该决策树判断特定场景适用的测试类型,从顶部开始匹配首个符合的条件。
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 TypeInfrastructureSpeedScopeWhen to Use
UnitNone (mocked/faked)<10ms per testSingle class/methodPure logic, domain rules, value objects, transformations, validators
IntegrationReal (DB, HTTP)100ms-5s per testMultiple componentsRepository queries, API contract verification, serialization round-trips, middleware behavior
E2E / FunctionalFull stack1-30s per testEntire request pipelineCritical user flows, auth + routing + middleware combined, cross-cutting concern verification
测试类型依赖基础设施情况速度测试范围适用场景
单元测试无依赖(全mocked/faked)单条测试<10ms单个类/方法纯逻辑、领域规则、值对象、数据转换、验证器
集成测试真实基础设施(数据库、HTTP服务等)单条测试100ms-5s多个组件仓库查询、API契约验证、序列化往返、中间件行为
E2E / 功能测试完整技术栈单条测试1-30s整个请求链路核心用户流程、鉴权+路由+中间件组合测试、横切关注点验证

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.

  • 业务逻辑优先使用单元测试:运行速度快,可精确定位故障,无基础设施依赖。
  • 使用集成测试验证基础设施边界的正确性:使用mocked
    DbContext
    的仓库单元测试无法验证实际SQL生成效果,应该通过Testcontainers连接真实数据库测试。
  • 谨慎使用E2E测试,仅覆盖核心路径:E2E测试运行慢、脆弱、维护成本高,只需覆盖主流程和1-2个核心故障场景即可。
  • 测试金字塔是指导原则而非硬性规则:部分应用(逻辑极少的CRUD API)用更多集成测试比单元测试收益更高,需要根据应用的复杂度特点匹配测试策略。

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.
tests/
目录下镜像
src/
的项目结构,添加后缀标识测试类型:
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]。

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

每个生产类对应一个测试类,测试文件的命名空间镜像生产代码的命名空间:
csharp
// Production: src/MyApp.Domain/Orders/OrderService.cs
// Test:       tests/MyApp.Domain.UnitTests/Orders/OrderServiceTests.cs
namespace MyApp.Domain.UnitTests.Orders;

public class OrderServiceTests
{
    // 按方法分组,再按场景分组
}
如果生产类代码量很大,可以按方法拆分测试类:
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):
StyleExample
Method_Scenario_Expected
CalculateTotal_EmptyCart_ReturnsZero
Should_Expected_When_Scenario
Should_ReturnZero_When_CartIsEmpty
Given_When_Then
GivenEmptyCart_WhenCalculatingTotal_ThenReturnsZero

使用
方法名_场景_预期行为
的命名模式,这样在测试资源管理器的输出中可读性好,故障信息自带文档效果:
csharp
public class OrderServiceTests
{
    [Fact]
    public void CalculateTotal_WithDiscountCode_AppliesPercentageDiscount()
    {
        // ...
    }

    [Fact]
    public void CalculateTotal_WithExpiredDiscount_ThrowsInvalidOperationException()
    {
        // ...
    }

    [Fact]
    public async Task SubmitOrder_WhenInventoryInsufficient_ReturnsOutOfStockError()
    {
        // ...
    }
}
可选命名风格(每个项目选择一种并保持一致即可):
风格示例
方法名_场景_预期结果
CalculateTotal_EmptyCart_ReturnsZero
Should_预期结果_When_场景
Should_ReturnZero_When_CartIsEmpty
Given_前提_When_操作_Then_结果
GivenEmptyCart_WhenCalculatingTotal_ThenReturnsZero

Arrange-Act-Assert Pattern

Arrange-Act-Assert模式

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.

所有测试都遵循AAA结构,保持三个部分清晰分隔:
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));
}
指导原则: 如果你无法清晰划分三个阶段,说明这个测试可能承担了过多职责,应该拆分为多个测试。

Test Doubles: When to Use What

测试替身:不同类型的适用场景

Terminology

术语定义

Double TypeBehaviorState VerificationUse When
StubReturns canned dataNoYou need a dependency to return specific values so the code under test can proceed
MockVerifies interactionsYes (interaction)You need to verify that the code under test called a dependency in a specific way
FakeWorking implementationYes (state)You need a lightweight but functional substitute (in-memory repository, in-memory message bus)
SpyRecords calls for later assertionYes (interaction)You need to verify calls happened without prescribing them upfront
替身类型行为状态验证适用场景
Stub返回预设数据你需要依赖返回特定值才能让被测代码正常执行时
Mock验证交互行为是(交互层面)你需要验证被测代码是否以特定方式调用了依赖时
Fake可运行的简化实现是(状态层面)你需要轻量但功能完整的替代实现时(内存仓库、内存消息总线)
Spy记录调用供后续断言是(交互层面)你需要验证调用确实发生,但不需要提前预设调用规则时

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 values)
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)

Example: Stub vs Mock vs Fake

示例:Stub vs Mock vs Fake

csharp
// 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;
    }
}
csharp
// STUB: 返回预设数据 -- 验证被测代码的逻辑
var priceService = Substitute.For<IPriceService>();
priceService.GetPriceAsync("SKU-001").Returns(29.99m);  // 预设返回值

var total = await calculator.CalculateTotalAsync(items);
Assert.Equal(59.98m, total);  // 对结果断言,而非对调用断言

// MOCK: 验证交互 -- 确认副作用已发生
var emailSender = Substitute.For<IEmailSender>();

await orderService.CompleteAsync(order);

await emailSender.Received(1).SendAsync(             // 对调用行为断言
    Arg.Is<string>(to => to == order.CustomerEmail),
    Arg.Any<string>(),
    Arg.Any<string>());

// FAKE: 内存实现 -- 无需真实基础设施即可模拟真实行为
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;
    }
}

When to Prefer Fakes Over Mocks

优先选择Fake而非Mock的场景

  • Domain-heavy applications: Fakes give more realistic behavior for complex interactions. An in-memory repository catches bugs that mocks miss (e.g., duplicate key violations).
  • Overuse of mocks is a test smell. If a test has more mock setup than actual assertions, consider whether a fake would be clearer and more maintainable.
  • Integration boundaries are better tested with real infrastructure via [skill:dotnet-integration-testing] than with mocks. A mocked
    DbContext
    does not verify that your LINQ translates to valid SQL.

  • 领域逻辑复杂的应用:对于复杂交互,Fake可以提供更真实的行为,内存仓库可以发现Mock漏掉的Bug(比如重复键冲突)。
  • 过度使用Mock是测试坏味道:如果一个测试里Mock配置的代码比实际断言还多,考虑用Fake会不会更清晰、更易维护。
  • 集成边界测试用真实基础设施效果更好:通过[skill:dotnet-integration-testing]使用真实基础设施测试,比用Mock效果好,Mock的
    DbContext
    无法验证你的LINQ是否可以转换为有效SQL。

Testing Anti-Patterns

测试反模式

1. Testing Implementation Details

1. 测试实现细节

csharp
// 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);
csharp
// 反面示例:重构内部实现时测试就会失效
repository.Received(1).GetByIdAsync(Arg.Is<Guid>(id => id == orderId));
repository.Received(1).SaveAsync(Arg.Any<Order>());
// ... 还有5个Received()调用验证精确的调用顺序

// 正面示例:测试可观测的输出
var result = await service.ProcessAsync(orderId);
Assert.Equal(OrderStatus.Completed, result.Status);

2. Excessive Mock Setup

2. 过多的Mock配置

csharp
// 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);
csharp
// 反面示例:Mock配置比实际测试代码还长
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行.Returns()配置代码

// 优化方案:使用Builder或者Fake封装配置
var fixture = new OrderServiceFixture()
    .WithOrder(testOrder)
    .WithPrice("SKU-001", 29.99m);
var result = await fixture.Service.ProcessAsync(testOrder.Id);

3. Non-Deterministic Tests

3. 非确定性测试

Tests must not depend on system clock, random values, or external network. Inject abstractions:
csharp
// 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));

测试不能依赖系统时钟、随机值或者外部网络,应该注入抽象:
csharp
// 反面示例:直接使用DateTime.UtcNow
public bool IsExpired() => ExpiresAt < DateTime.UtcNow;

// 正面示例:注入TimeProvider(.NET 8+支持)
public bool IsExpired(TimeProvider time) => ExpiresAt < time.GetUtcNow();

// 测试中使用
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 6, 15, 0, 0, 0, TimeSpan.Zero));
Assert.True(order.IsExpired(fakeTime));

Key Principles

核心原则

  • Test behavior, not implementation. Assert on observable outcomes (return values, state changes, published events), not internal method calls.
  • One logical assertion per test. Multiple
    Assert
    calls are fine if they verify one logical concept (e.g., all properties of a returned object). Multiple unrelated assertions indicate the test should be split.
  • Keep tests independent. No test should depend on another test's execution or ordering. Use fresh fixtures for each test.
  • Name tests so failures are self-documenting. A failing test name should tell you what broke without reading the test body.
  • Match test type to risk. High-risk code (payments, auth) deserves integration and E2E coverage. Low-risk code (simple mapping) needs only unit tests.
  • Use
    TimeProvider
    for time-dependent logic
    (.NET 8+). It is the framework-provided abstraction; do not create custom
    IClock
    interfaces.

  • 测试行为,而非实现:对可观测的结果(返回值、状态变更、发布的事件)断言,而非对内部方法调用断言。
  • 每个测试只做一个逻辑断言:多个
    Assert
    调用没问题,只要它们验证的是同一个逻辑概念(比如返回对象的所有属性),多个不相关的断言说明测试应该拆分。
  • 保持测试独立:任何测试都不应该依赖其他测试的执行结果或者执行顺序,每个测试使用全新的fixture。
  • 测试命名要让故障自带文档:失败的测试名称应该能直接告诉你哪里出了问题,不需要阅读测试代码。
  • 测试类型匹配风险等级:高风险代码(支付、鉴权)需要集成测试和E2E测试覆盖,低风险代码(简单映射)只需要单元测试即可。
  • 时间相关逻辑使用
    TimeProvider
    (.NET 8+):这是框架提供的官方抽象,不需要自定义
    IClock
    接口。

Agent Gotchas

Agent注意事项

  1. Do not mock types you do not own. Mocking
    HttpClient
    ,
    DbContext
    , or framework types leads to brittle tests that do not reflect real behavior. Use
    WebApplicationFactory
    or Testcontainers instead -- see [skill:dotnet-integration-testing].
  2. Do not create test projects without checking for existing structure. Run [skill:dotnet-project-analysis] first; duplicating test infrastructure causes build conflicts.
  3. Do not use
    Thread.Sleep
    in tests.
    Use
    Task.Delay
    with a cancellation token, or better, use
    FakeTimeProvider.Advance()
    to control time deterministically.
  4. Do not test private methods directly. If a private method needs its own tests, it should be extracted into its own class. Test through the public API.
  5. Do not hard-code connection strings in integration tests. Use Testcontainers for disposable infrastructure or
    WebApplicationFactory
    for in-process testing -- see [skill:dotnet-integration-testing].

  1. 不要Mock你不拥有的类型:Mock
    HttpClient
    DbContext
    或者框架类型会导致测试脆弱,无法反映真实行为,应该使用
    WebApplicationFactory
    或者Testcontainers,参考[skill:dotnet-integration-testing]。
  2. 创建测试项目前先检查现有结构:先运行[skill:dotnet-project-analysis],重复的测试基础设施会导致构建冲突。
  3. 测试中不要使用
    Thread.Sleep
    :使用带取消令牌的
    Task.Delay
    ,更好的方式是用
    FakeTimeProvider.Advance()
    确定性控制时间。
  4. 不要直接测试私有方法:如果私有方法需要单独测试,说明它应该被提取到独立的类中,通过公共API测试即可。
  5. 集成测试中不要硬编码连接字符串:使用Testcontainers创建 disposable 基础设施,或者用
    WebApplicationFactory
    做进程内测试,参考[skill:dotnet-integration-testing]。

References

参考资料