资讯动态

dotnet-starter-kit 测试指南:xUnit + Shouldly + NSubstitute + AutoFixture 的单元与集成测试实战

发布时间:2026/9/17 19:17:05 来源:尧图企业网站定制
dotnet-starter-kit 测试指南xUnit Shouldly NSubstitute AutoFixture 的单元与集成测试实战【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200 Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit导读本文是 dotnet-starter-kit 仓库中.agents/skills/testing-guide/SKILL.md的完整展开。该指南是仓库团队为 FSHFullStackHero模块化架构编写的测试规范明确了测试技术栈xUnit Shouldly NSubstitute AutoFixture、命名与 AAA 约定、Handler / Validator / 领域实体三类测试的模板以及 NetArchTest 架构守卫测试与 Testcontainers 集成测试的运行方式。读完本文你将能够按仓库既有约定为任意 FSH 功能编写可被 CI 持续校验的单测理解dotnet test各命令的适用场景并避开集成测试中 AsyncLocal 租户上下文、存储服务重接线等经典陷阱。1. 测试技术栈与约定总览SKILL.md 开头即明确了仓库的测试技术选型xUnitShouldly.ShouldBeNSubstituteSubstitute.ForAutoFixturenew Fixture()。不使用Moq不使用FluentAssertions。各组件职责划分如下组件用途典型写法xUnit测试框架[Fact]、[Theory][InlineData(...)]Shouldly断言库result.ShouldBe(...)、id.ShouldNotBe(Guid.Empty)、list.ShouldContain(...)NSubstitute模拟依赖Substitute.ForIUserService()、Received(1).X(...)AutoFixture测试数据生成new Fixture().CreateT()、_fixture.Createstring()NetArchTest架构规则测试位于Architecture.Tests见第 5 节Testcontainers集成测试基础设施用于 PostgreSQL / Redis / MinIO见第 6 节完整约定细节存放在.agents/rules/testing.md与.agents/rules/integration-testing.mdSKILL.md 是它们的速查入口。1.1 命名与结构约定测试类public sealed class {Sut}Tests被测对象System Under Test字段统一命名为_sut测试方法名MethodName_Should_ExpectedBehavior[_When_Condition]即方法名 应产生 预期行为 在什么条件下Arrange-Act-Assert用// Arrange/// Act/// Assert注释明确分段并用#region按场景分组Happy Path / Guards / Edge Cases模拟通过Substitute.ForIService()创建替身用.Received(1).X(arg, Arg.AnyCancellationToken())断言调用次数与参数CancellationToken 陷阱当被测代码把外部传入的CancellationToken转发给下游服务时必须断言具体的 token 实例而非默认值——NSubstitute 会用default填充可选参数因此Received(1).XAsync(arg)会静默地断言成CancellationToken.None掩盖真实的转发行为。2. Handler 测试命令/查询处理器Handler命令/查询处理器是 FSH 模块化架构中承载业务逻辑的核心对象也是单元测试覆盖最密集的一层。仓库把测试项目按模块拆分src/Tests/{Module}.Tests例如Billing.Tests、Identity.Tests、Catalog.Tests等见 src/Tests 目录。2.1 DbContext 依赖的 HandlerSKILL.md 给出的模板展示了带 EF DbContext 依赖的 Handler 测试骨架public sealed class Create{Entity}CommandHandlerTests { private readonly {X}DbContext _db; // or Substitute.ForIService() for service deps private readonly Create{Entity}CommandHandler _sut; private readonly IFixture _fixture new Fixture(); public Create{Entity}CommandHandlerTests() { _db /* in-memory or test DbContext */; _sut new Create{Entity}CommandHandler(_db); } [Fact] public async Task Handle_Should_PersistEntity_And_ReturnId() { // Arrange var command new Create{Entity}Command(_fixture.Createstring(), 9.99m, USD); // Act var id await _sut.Handle(command, CancellationToken.None); // Assert id.ShouldNotBe(Guid.Empty); } }要点解读依赖注入方式优先使用内存或测试专用的 DbContext对于服务类依赖则用 NSubstitute 替身二者通过构造函数手动组装_sut不走 DI 容器AutoFixture 生成数据_fixture.Createstring()用于生成随机合法字符串避免手写样板数据金额等敏感字段则显式给出字面量9.99m, USD保证断言可预测行为断言id.ShouldNotBe(Guid.Empty)验证的是命令成功执行并产生了新 ID这一可观察行为而不是去探测内部状态。2.2 服务依赖的 HandlerNSubstitute 示例当 Handler 依赖服务接口时用 NSubstitute 替身并断言其被正确调用_userService Substitute.ForIUserService(); // Act … then: await _userService.Received(1).ToggleStatusAsync(true, command.UserId, Arg.AnyCancellationToken());Received(1)断言恰好调用一次Arg.AnyCancellationToken()与 2.1 节的具体 token 断言规则结合使用——若需要验证 token 确实被转发则把Arg.AnyCancellationToken()换成构造测试时传入的具体ct实例。3. Validator 测试FluentValidation 校验器Validator 使用 xUnit 的[Theory]参数化测试来覆盖非法/合法输入边界。SKILL.md 模板如下public sealed class Create{Entity}CommandValidatorTests { private readonly Create{Entity}CommandValidator _sut new(); [Theory] [InlineData()] public void Validate_Should_Fail_When_NameInvalid(string name) { var result _sut.Validate(new Create{Entity}Command(name, 1m, USD)); result.IsValid.ShouldBeFalse(); result.Errors.ShouldContain(e e.PropertyName nameof(Create{Entity}Command.Name)); } }关键点Validator 无依赖可直接new因此字段初始化即完成 SUT 组装断言分两层result.IsValid.ShouldBeFalse()校验整体结果result.Errors.ShouldContain(e e.PropertyName ...)校验具体哪个属性报错防止校验器错在别处却通过测试用nameof(...)引用属性名而非硬编码字符串重构安全。仓库中的真实示例可参考 src/Tests/Billing.Tests/Validators/CreateTopupRequestValidatorTests.cs它对钱包充值金额做了边界测试[Theory] [InlineData(0)] [InlineData(-5)] [InlineData(1_000_001)] public void Rejects_out_of_range(decimal amount) _v.Validate(new CreateTopupRequestCommand(amount, null)).IsValid.ShouldBeFalse(); [Fact] public void Accepts_valid_amount() _v.Validate(new CreateTopupRequestCommand(50m, need credit)).IsValid.ShouldBeTrue();注意该真实用例的方法名Rejects_out_of_range/Accepts_valid_amount与 SKILL.md 的完整命名规范略有简化但行为驱动、参数化覆盖边界的思想完全一致——实际编写时以MethodName_Should_ExpectedBehavior_When_Condition全格式为准。4. 实体/领域测试不依赖任何 Mock领域实体Entity / Aggregate的测试是纯内存操作完全不使用 Mock直接通过静态工厂方法构造实体并断言不变量与领域事件[Fact] public void Create_Should_RaiseCreatedEvent() { var entity {Entity}.Create(Test, Money.Zero()); entity.Id.ShouldNotBe(Guid.Empty); entity.DomainEvents.ShouldContain(e e is {Entity}CreatedDomainEvent); }这类测试与仓库的领域建模规范见.agents/skills/add-entity/SKILL.md深度呼应实体继承AggregateRootGuid或BaseEntityGuid通过私有 EF 构造函数静态Create工厂方法创建保证不变量集中校验静态工厂内部用Guid.CreateVersion7()生成 ID并通过DomainEvent.Create((id, ts) ...)AddDomainEvent(...)发布领域事件如{Entity}CreatedDomainEvent因此创建实体 → 断言 ID 非空 领域事件集合包含 Created 事件是验证工厂契约是否完整的最直接手段同时也是对DomainEvents基础设施见 src/BuildingBlocks/Core/Domain/DomainEvent.cs的行为验证。从仓库测试分布看{Module}.Tests/Domain/子目录集中存放这类用例如 src/Tests/Billing.Tests/Domain、src/Tests/Catalog.Tests/Domain、src/Tests/Files.Tests/Domain各模块的实体行为测试保持同一模式。5. 架构测试用 NetArchTest 守住模块边界SKILL.md 明确Architecture.Tests基于 NetArchTest是必须保持绿色的守卫测试它强制以下规则模块边界跨模块引用只能通过.Contracts不能直接引用其他模块的实现程序集租户隔离规则实体上的租户隔离约束见 src/Tests/Architecture.Tests/TenantIsolationTests.csHandler 必须 sealed每个 Command Handler 与分页 Query Handler 都必须有对应的 Validator见 src/Tests/Architecture.Tests/HandlerValidatorPairingTests.cs。SKILL.md 特别强调不要为了让改动通过测试而削弱这些规则——应该修复代码本身。架构测试是防退化护栏而非可讨价还价的清单。以HandlerValidatorPairingTests为例其实现思路值得借鉴通过反射扫描各模块程序集找出所有实现ICommandHandler/ICommandHandler,的类型从泛型实参提取 Command 类型名推导期望的 Validator 名{Cmd}Validator/{Name}CommandValidator/{Name}Validator三种命名均被接受对分页查询含PageNumber/PageSize/Skip/Take属性的 Query要求同样存在 Validator 以约束分页边界反向检查 Validator 的命名是否与所校验的 Command/Query 匹配揪出孤儿 Validator对少数已知缺 Validator 的 Handler仓库在KnownMissingCommandHandlers/KnownMissingQueryHandlers白名单中显式登记作为待补清单管理。其余架构用例还包括ModuleArchitectureTests、LayerDependencyTests、CircularReferenceTests、NamespaceConventionsTests等见 src/Tests/Architecture.Tests共同构成模块边界 分层依赖 命名约定 配对完整性的多维护栏。6. 集成测试Testcontainers 拉起真实基础设施Integration.Tests通过WebApplicationFactory运行在真实PostgreSQL / Redis / MinIO 之上Testcontainers 提供容器化基础设施因此必须安装并启动 Docker。若 Docker 未运行测试会以DockerUnavailableException快速失败——这是环境问题而非代码回归此时应改跑单元测试项目验证逻辑。测试项目矩阵来自.agents/rules/testing.md项目范围需要 Docker{Module}.Tests单元Handlers、Services、Domain否Framework.Tests、Generic.Tests、Caching.TestsBuildingBlocks 单元否Architecture.TestsNetArchTest模块边界 租户隔离 Handler↔Validator 配对否Integration.TestsWebApplicationFactory跑真实 PostgreSQL/Redis/MinIO是Integration.Middleware.Tests真实中间件链路是6.1 基础设施装配FshWebApplicationFactory位于src/Tests/Integration.Tests/Infrastructure/负责启动容器、叠加内存配置、把IMailService替换为NoOpMailService、并把存储重接线到 MinIO。6.2 三个必知的坑租户上下文是 AsyncLocal——必须在方法内联设置。Finbuckle 租户上下文要放在调用UserManager/DbContext的同一个测试方法里设置若放进一个被await的辅助方法跨 async 边界后会丢失租户上下文导致租户查询过滤器抛出 NRE见.agents/rules/integration-testing.md。存储服务是急切接线。AddHeroStorage会在测试配置叠加之前读取Storage:Provider因此默认选中LocalStorageService。工厂需要在注册后移除IStorageService/LocalStorageService/S3StorageService的 descriptor再重新注册 S3 栈指向 MinIO。需要真实对象存储的测试须遵循该模式详见.agents/rules/storage.md。SignalR 测试强制使用长轮询long-polling。TestServer 没有 WebSocket客户端传输必须显式配置为长轮询否则连接失败。另有额外一条来自.agents/rules/integration-testing.md限流配置是急切读取的Integration.Middleware.Tests必须在宿主构建之前通过环境变量设置RateLimitingOptions:Enabled事后翻转不生效。7. 运行测试与覆盖率SKILL.md 与.agents/rules/testing.md给出了三档运行命令# 运行单个单元测试项目不需要 Docker dotnet test src/Tests/{X}.Tests # 运行架构守卫测试 dotnet test src/Tests/Architecture.Tests # 运行全部测试并收集覆盖率集成测试需要 Docker dotnet test src/FSH.Starter.slnx --collect XPlat Code Coverage --settings coverage.runsettings补充说明全量运行可直接dotnet test src/FSH.Starter.slnxslnx 解决方案文件位于 src/FSH.Starter.slnx覆盖率收集用--collect XPlat Code Coverage --settings coverage.runsettings前端React测试采用 Playwright 路由 Mock不连真实后端入口为cd clients/{app} npm run test:e2e详见.agents/rules/frontend/shared.mdclients/admin与clients/dashboard两个前端各有对应的tests/目录与 playwright.config.ts。7.1 覆盖率配置解读coverage.runsettings 定义了覆盖率口径直接决定有意义覆盖率的分母Formatcobertura/Format Include[FSH.Modules.*]*,[FSH.Framework.*]*/Include Exclude[*.Tests]*,[*Tests]*,[FSH.Starter.Migrations.*]*,[*]*.Migrations.*,[*]*HostedService/Exclude ExcludeByAttributeGeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute,DebuggerNonUserCodeAttribute/ExcludeByAttribute SkipAutoPropstrue/SkipAutoPropsInclude只统计产品代码FSH.Modules.*各业务模块 FSH.Framework.*各 BuildingBlocks 框架程序集Exclude剔除测试程序集、Migrations 项目以及*HostedService——后台/托管服务是长运行循环不由请求流或单元驱动混入会稀释覆盖率分母ExcludeByAttribute排除自动生成代码SkipAutoProps跳过自动属性避免被{ get; set; }撑高覆盖率假象。8. 实战速查清单为某个 FSH 功能新增测试时按此顺序检查与.agents/rules/testing.md、.agents/skills/add-feature/SKILL.md等配套技能配合使用选对项目Handler/Validator/领域逻辑 →src/Tests/{Module}.Tests框架层 →Framework.Tests/Generic.Tests/Caching.Tests涉及 DB/HTTP 管线 →Integration.Tests遵守命名public sealed class {Sut}Tests、SUT 字段_sut、方法名MethodName_Should_ExpectedBehavior_When_ConditionAAA 三段式// Arrange/// Act/// Assert用#regionHappy Path / Guards / Edge Cases分组Mock 与断言只认 NSubstituteSubstitute.ForReceived(1).X(...)不用 Moq断言用 Shouldly不用 FluentAssertions转发CancellationToken时断言具体 token架构规则Command 与分页 Query 必须有同名 Validator不要改动Architecture.Tests放行自己的代码集成测试确认 Docker 在线租户上下文内联设置存储需按 MinIO 重接线模式处理SignalR 用长轮询限流开关在宿主构建前设好验证命令先跑dotnet test src/Tests/{X}.Tests与dotnet test src/Tests/Architecture.Tests最后全量跑dotnet test src/FSH.Starter.slnx。遵循以上约定编写的测试既能在单个模块内保证行为正确也能让整个仓库的模块边界、租户隔离与Handler↔Validator配对等架构约束持续保持绿色。【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200 Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价