- 后端
- 电商
- 前端
- 微服务
【免费下载链接】eShop
A reference .NET application implementing an eCommerce site
本指南以 tests/README.md 为骨架,系统梳理 eShop 参考实现(.NET 电商站点)中用于验证各业务组件行为的测试体系。仓库tests/目录下同时包含单元测试与功能测试两类工程:前者以 MSTest + NSubstitute 对服务与领域逻辑做快速隔离验证,后者借助.NET Aspire 托管测试容器(如 PostgreSQL)在真实依赖环境下端到端验证 API 行为。读完本文,你将掌握 eShop 各测试项目的结构、核心测试手法、Aspire 测试基础设施的搭建方式,以及运行这些测试所需的前置条件(Docker)。
一、测试体系总览:单元测试与功能测试的分工
原文档开宗明义地指出:tests/目录存放一组用于验证 eShop 应用各组件行为的单元测试(unit tests)与功能测试(functional tests)。结合仓库实际布局,这两个层次的分工非常清晰:
| 层次 | 测试项目 | 验证对象 | 依赖 |
|---|---|---|---|
| 单元测试 | Basket.UnitTests | Basket.API 的 gRPC 服务逻辑 | NSubstitute Mock + 无外部依赖 |
| 单元测试 | Ordering.UnitTests | Ordering 领域模型与命令处理器 | NSubstitute Mock + 领域模型 |
| 单元测试 | ClientApp.UnitTests | MAUI 客户端服务层 | 内置 Mock 服务 |
| 功能测试 | Catalog.FunctionalTests | Catalog.API 的 REST API 全流程 | Aspire + PostgreSQL 测试容器 |
| 功能测试 | Ordering.FunctionalTests | Ordering.API 的下单、取消、发货等流程 | Aspire + PostgreSQL + Identity API |
原文档还特别强调了一条重要前置条件:功能测试依赖 Aspire host 启动测试容器,因此要求 Docker 处于运行状态。这一点在所有功能测试项目中都有直接体现(详见下文第四节)。
从 tests/Directory.Build.props 可以看到,所有测试项目共享统一的测试框架配置:
<Project> <!-- MSTest settings --> <PropertyGroup> <MSTestAnalysisMode>Recommended</MSTestAnalysisMode> </PropertyGroup> <!-- xUnit settings --> <PropertyGroup> <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner> </PropertyGroup> </Project>该文件通过目录级属性(Directory.Build.props)为整个tests/子树启用了 MSTest 的推荐分析模式(MSTestAnalysisMode=Recommended),并统一开启 Microsoft Testing Platform runner(UseMicrosoftTestingPlatformRunner=true),这意味着所有测试项目均以Exe输出类型运行于统一的测试执行平台之上。
二、单元测试项目详解:隔离验证服务与领域逻辑
2.1 Basket.UnitTests:gRPC 服务层的 Mock 测试
Basket.UnitTests 以 MSTest.Sdk 为项目骨架,通过 NSubstitute 对仓储层进行替换,验证 BasketService 的 gRPC 方法行为(详见 Basket.UnitTests.csproj 与 BasketServiceTests.cs)。
核心测试思路可从 BasketServiceTests.cs 中提炼:
[TestClass] public class BasketServiceTests { public TestContext TestContext { get; set; } [TestMethod] public async Task GetBasketReturnsEmptyForNoUser() { var mockRepository = Substitute.For<IBasketRepository>(); var service = new BasketService(mockRepository, NullLogger<BasketService>.Instance); var serverCallContext = TestServerCallContext.Create(cancellationToken: TestContext.CancellationToken); serverCallContext.SetUserState("__HttpContext", new DefaultHttpContext()); var response = await service.GetBasket(new GetBasketRequest(), serverCallContext); Assert.IsInstanceOfType<CustomerBasketResponse>(response); Assert.IsEmpty(response.Items); } }三个测试用例恰好覆盖了三种关键路径:
- 无用户上下文:
GetBasketReturnsEmptyForNoUser验证未携带用户信息时返回空购物篮; - 有效用户 ID:
GetBasketReturnsItemsForValidUserId通过ClaimsPrincipal注入sub声明,验证能取回对应条目的购物篮; - 无效用户 ID:
GetBasketReturnsInvalidUserId验证即便仓储有数据,无有效身份时仍返回空结果。
测试的关键难点在于 gRPC 的ServerCallContext在单元测试中难以直接构造,仓库通过 TestServerCallContext.cs 提供帮助器来模拟。同时测试用NullLogger<BasketService>.Instance代替真实日志器,将关注点完全收敛到业务逻辑本身。
2.2 Ordering.UnitTests:命令处理器与领域聚合测试
Ordering.UnitTests 是覆盖面最广的单元测试项目,引用 Ordering.API、Ordering.Domain、Ordering.Infrastructure 三个项目(见 Ordering.UnitTests.csproj),测试分布在Application/与Domain/两个命名空间下:
- Application 层:测试 CQRS 命令处理器,例如 NewOrderCommandHandlerTest.cs 通过 NSubstitute Mock 掉
IOrderRepository、IIdentityService、IMediator、IOrderingIntegrationEventService,验证CreateOrderCommandHandler.Handle在订单未持久化时返回false,并验证空 Buyer 构造时抛出ArgumentNullException;IdentifiedCommandHandlerTest.cs 与 SetStockRejectedOrderStatusCommandTest.cs 则覆盖幂等命令与库存驳回状态流转;OrdersWebApiTest.cs 验证 Web API 映射层; - Domain 层:BuyerAggregateTest.cs 与 OrderAggregateTest.cs 直接对 DDD 聚合根进行行为验证,ValueObjectTests.cs 则验证值对象相等性语义。
以订单命令处理器测试为例,其 Mock 装配方式是典型的 eShop 单元测试模式:
_orderRepositoryMock = Substitute.For<IOrderRepository>(); _identityServiceMock = Substitute.For<IIdentityService>(); _orderingIntegrationEventService = Substitute.For<IOrderingIntegrationEventService>(); _mediator = Substitute.For<IMediator>(); // 模拟仓储层保存成功 _orderRepositoryMock.UnitOfWork.SaveChangesAsync(default) .Returns(Task.FromResult(1));从源码结构可以看出,Ordering 的单元测试有意保持与基础设施(EF Core、消息总线)完全解耦,让领域规则与命令编排可以在毫秒级完成验证。
2.3 ClientApp.UnitTests:MAUI 客户端服务层测试
ClientApp.UnitTests 是唯一针对移动客户端(.NET MAUI)的测试项目,其 csproj 设置了UseMaui=true并引用 src/ClientApp/ClientApp.csproj(见 ClientApp.UnitTests.csproj)。
测试对象是客户端封装的 HTTP 服务层与 ViewModel。以 CatalogServiceTests.cs 为例,它直接使用CatalogMockService(客户端内置的假数据服务)验证目录、品牌、类型三个数据源均能返回非空集合:
[TestMethod] public async Task GetFakeCatalogTest() { var catalogMockService = new CatalogMockService(); var catalog = await catalogMockService.GetCatalogAsync(); Assert.AreNotEqual(0, catalog.Count()); }项目还内置了 MockDialogService.cs、MockNavigationService.cs、MockSettingsService.cs 等一组 Mock 服务,用于支撑 MainViewModelTests.cs、CatalogViewModelTests.cs、CatalogItemViewModelTests.cs、OrderViewModelTests.cs 等 ViewModel 层测试,覆盖购物篮服务、目录服务、订单服务三条客户端业务线(对应 Services 目录下的BasketServiceTests.cs、CatalogServiceTests.cs、OrdersServiceTests.cs)。
三、功能测试的 Aspire 基础设施:测试容器的关键原理
原文档着重指出功能测试"leverage the Aspire host to spin up test containers"。这一机制在 CatalogApiFixture.cs 中体现得最为直接。该 Fixture 继承WebApplicationFactory<Program>并实现IAsyncLifetime,在构造函数中使用DistributedApplication.CreateBuilder启动一个精简的 Aspire host:
public sealed class CatalogApiFixture : WebApplicationFactory<Program>, IAsyncLifetime { private readonly IHost _app; public CatalogApiFixture() { var options = new DistributedApplicationOptions { AssemblyName = typeof(CatalogApiFixture).Assembly.FullName, DisableDashboard = true // 测试环境关闭 Aspire 仪表盘 }; var appBuilder = DistributedApplication.CreateBuilder(options); Postgres = appBuilder.AddPostgres("CatalogDB") .WithImage("ankane/pgvector") .WithImageTag("latest"); _app = appBuilder.Build(); } ... }这段代码揭示了三个关键设计:
- 测试即 Aspire 编排:测试进程本身就是一个简化版 Aspire 应用,通过
AddPostgres("CatalogDB")声明 PostgreSQL 资源,Aspire 会负责在 Docker 中拉起容器并管理其生命周期; - 选用 pgvector 镜像:Catalog 功能测试使用的镜像为
ankane/pgvector,这与 Catalog.API 中商品目录的语义化检索(semantic search)能力相匹配——测试容器需要具备向量存储扩展; - 关闭仪表盘:
DisableDashboard = true表明测试环境刻意避免 Aspire Dashboard 的开销,只保留资源编排能力。
InitializeAsync中执行await _app.StartAsync()并在之后通过Postgres.Resource.GetConnectionStringAsync()获取容器就绪后的真实连接串;CreateHost则将该连接串以ConnectionStrings:CatalogDB的形式注入被测应用的配置:
protected override IHost CreateHost(IHostBuilder builder) { builder.ConfigureHostConfiguration(config => { config.AddInMemoryCollection(new Dictionary<string, string> { { $"ConnectionStrings:{Postgres.Resource.Name}", _postgresConnectionString }, }); }); return base.CreateHost(builder); }这正是"在真实数据库上跑 API"的实现路径:被测的Program(来自 src/Catalog.API)以容器数据库的连接串启动,功能测试访问的是与生产行为一致的完整技术栈。
对应的 Catalog.FunctionalTests.csproj 使用的 SDK 为Aspire.AppHost.Sdk/13.2.0,目标框架为net10.0,并引用了Aspire.Hosting.PostgreSQL(容器编排)、Microsoft.AspNetCore.Mvc.Testing与Microsoft.AspNetCore.TestHost(进程内托管被测应用)、Asp.Versioning.Http.Client(API 版本化客户端)以及xunit.v3.mtp-v2(xUnit v3 + Microsoft Testing Platform 运行器)。注意其中对 Catalog.API 的项目引用带有IsAspireProjectResource="false",明确告知 Aspire 不要将被测项目当作可编排资源另行启动,而是作为进程内程序集引用。
四、功能测试实战:Catalog 与 Ordering 的端到端验证
4.1 Catalog.FunctionalTests:覆盖 v1/v2 双版本 API
CatalogApiTests.cs 通过IClassFixture<CatalogApiFixture>复用同一个 Fixture(同一测试容器实例),并使用ApiVersionHandler+QueryStringApiVersionWriter构造支持 API 版本化的 HttpClient:
private HttpClient CreateHttpClient(ApiVersion apiVersion) { var handler = new ApiVersionHandler(new QueryStringApiVersionWriter(), apiVersion); return _webApplicationFactory.CreateDefaultClient(handler); }几乎所有用例都以[Theory]+[InlineData(1.0)]/[InlineData(2.0)]的形式同时验证 v1 与 v2 两个 API 版本,且两种版本往往使用不同的路由形态(例如 v1 的/api/catalog/items/by/{name}在 v2 中演化为/api/catalog/items?name=)。代表性用例及其验证点包括:
| 测试用例 | 验证内容 |
|---|---|
GetCatalogItemsRespectsPageSize | 分页语义:pageIndex=0&pageSize=5返回 5 条,且断言库中商品总数(101 条种子数据 + 2 条由 AddCatalogItem 用例新增 = 103) |
UpdateCatalogItemWorksWithoutPriceUpdate/WithPriceUpdate | 更新库存/价格后能正确回读,价格变更用例断言Price更新为1.99m |
GetCatalogItemsbyIds | ids=1&ids=2&ids=3多 ID 查询返回 3 条 |
GetCatalogItemWithExactName/WithPartialName | 精确名称("Wanderer Black Hiking Boots" 返回 1 条)与模糊名称("Alpine" 返回 4 条)检索 |
GetCatalogItemWithsemanticrelevance | 语义相关性检索端点(withsemanticrelevance)可用 |
GetCatalogItemWithTypeIdBrandId/GetAllCatalogTypeItemWithBrandId | 类型 + 品牌组合过滤(type=3/brand=3 返回 4 条;brand=3 全类型返回 11 条) |
GetCatalogItemPicWithId | 商品图片端点返回image/webp内容类型 |
GetAllCatalogTypes/GetAllCatalogBrands | 元数据:8 个类型、13 个品牌 |
AddCatalogItem/DeleteCatalogItem | 完整生命周期:新增后可查询到(v1/v2 使用不同 ID),删除后返回NoContent、再查询返回NotFound |
其中AddCatalogItem用例构造的完整CatalogItem负载(含AvailableStock、RestockThreshold、MaxStockThreshold、OnReorder等库存字段)与 CatalogItem.cs 中的模型定义一一对应,可以直接作为调用 Catalog API 的参考报文。值得注意的是,GetCatalogItemsRespectsPageSize断言总数为 103,这一数据依赖测试的线性执行顺序(先AddCatalogItem后分页查询),从测试设计上看属于功能测试中的隐含执行顺序约束。
4.2 Ordering.FunctionalTests:多服务编排与自动身份注入
Ordering.FunctionalTests 是依赖最复杂的功能测试:其 Fixture 不仅拉起订单数据库容器,还编排了第二个 PostgreSQL 容器(IdentityDB)以及 Identity.API 项目本身:
Postgres = appBuilder.AddPostgres("OrderingDB"); IdentityDB = appBuilder.AddPostgres("IdentityDB"); IdentityApi = appBuilder.AddProject<Projects.Identity_API>("identity-api") .WithReference(IdentityDB);OrderingApiFixture.cs 在CreateHost中把Postgres连接串注入ConnectionStrings:OrderingDB,把IdentityApi.GetEndpoint("http").Url注入Identity:Url配置,并注册了一个AutoAuthorizeStartupFilter来注入测试专用的认证中间件。
AutoAuthorizeMiddleware.cs 是 Ordering 功能测试的关键设计:由于下单接口需要用户身份,测试通过中间件为每个请求伪造一个固定身份的ClaimsIdentity:
public const string IDENTITY_ID = "9e3163b9-1ae6-4652-9dc6-7898ab7b7a00"; public async Task Invoke(HttpContext httpContext) { var identity = new ClaimsIdentity("cookies"); identity.AddClaim(new Claim("sub", IDENTITY_ID)); identity.AddClaim(new Claim("unique_name", IDENTITY_ID)); identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); httpContext.User.AddIdentity(identity); await _next.Invoke(httpContext); }这样 OrderingApiTests.cs 中的用例无需真实登录流程即可覆盖受保护端点,同时测试真实走通身份解析链路(sub声明由 ServerCallContextIdentityExtensions 一类的扩展解析)。Ordering 功能测试覆盖的典型场景包括:
- 正常下单:
AddNewOrder用CreateOrderRequest+x-requestid请求头提交,断言返回OK; - 草稿订单:
PostDraftOrder、CreateOrderDraftSucceeds验证订单草稿计算逻辑,后者甚至断言Total等于Σ(Quantity × UnitPrice),并对订单项产品 ID 与请求负载做一致性比对; - 失败路径:
AddNewEmptyOrder(空订单返回BadRequest)、CancelWithEmptyGuidFails/ShipWithEmptyGuidFails(x-requestid为空 Guid 返回BadRequest)、CancelNonExistentOrderFails/ShipNonExistentOrderFails(不存在的订单返回InternalServerError); - 查询路径:
GetAllStoredOrdersWorks、GetAllOrdersCardType、GetStoredOrdersWithOrderId(访问不存在的订单 ID 返回NotFound)。
这里x-requestid请求头正是 eShop 幂等性设计的核心载体——下游 Idempotency/RequestManager.cs 会基于该请求 ID 对命令去重。功能测试对空 Guid 与非空 Guid 的区分验证,恰好覆盖了幂等命令验证器的行为边界(对应 IdentifiedCommandHandlerTest.cs 的单元层面验证,两层测试形成互补)。
五、运行测试:命令、前置条件与适用限制
5.1 单元测试:零依赖,即开即跑
单元测试(Basket.UnitTests、Ordering.UnitTests、ClientApp.UnitTests)不依赖任何外部服务,直接通过dotnet test执行即可。例如只跑购物篮服务测试:
dotnet test tests/Basket.UnitTests/Basket.UnitTests.csproj运行整个解决方案的测试:
dotnet test eShop.slnx由于各测试项目均为net10.0目标框架且启用了 Microsoft Testing Platform runner,运行环境需要与仓库 global.json 声明匹配的 .NET 10 SDK;MAUI 客户端测试(ClientApp.UnitTests)还要求具备 .NET MAUI 工作负载。
5.2 功能测试:必须先启动 Docker
功能测试(Catalog.FunctionalTests、Ordering.FunctionalTests)的运行链路为:测试进程启动 Aspire host → Aspire 向 Docker 请求创建ankane/pgvector(Catalog)或标准 PostgreSQL(Ordering)容器 → 等待容器就绪并取得连接串 → 进程内启动被测 API。因此:
- Docker 必须正在运行——这是 tests/README.md 明确声明的前置条件,否则
_app.StartAsync()阶段将因无法创建容器而失败; - 首次运行会拉取镜像,取决于网络环境可能需要一定时间;Catalog 功能测试使用
WithImageTag("latest")拉取最新版ankane/pgvector镜像; - 测试容器默认不共享 Dashboard(
DisableDashboard = true),测试结束通过DisposeAsync依次停止并释放 Aspire host 与被测应用。
运行单个功能测试项目:
dotnet test tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj5.3 测试覆盖的边界(从源码结构看)
从仓库源码结构可以推断 eShop 测试体系目前的设计边界:
- API 服务层覆盖充分:Catalog、Ordering 的 REST 端点均有功能测试,Basket 的 gRPC 服务有单元测试;
- 订单领域逻辑覆盖充分:聚合根、值对象、命令处理器、幂等命令均有单元测试;
- 客户端以 Mock 数据为主:ClientApp.UnitTests 通过内置 Mock 服务验证服务与 ViewModel 逻辑,并未真实启动后端 API;
- 集成事件链路依赖功能测试间接覆盖:如 Ordering 下单后发布
OrderStartedIntegrationEvent的环节(见 OrderStartedIntegrationEventHandler.cs),在单元测试中通过 Mock 的IOrderingIntegrationEventService隔离,其真实链路正确性更多依赖功能测试的整体通过。
六、给读者的实操要点总结
- 想快速验证业务规则:跑 Ordering.UnitTests 与 Basket.UnitTests,它们完全隔离、秒级完成,且测试命名清晰(
GetBasketReturnsEmptyForNoUser等),本身就是行为文档; - 想验证 API 契约与数据层:跑 Catalog.FunctionalTests,它直接以
ankane/pgvector容器验证包含语义检索在内的完整目录能力,并同时覆盖 v1/v2 两代 API; - 想验证订单核心链路:跑 Ordering.FunctionalTests,它编排了 OrderingDB、IdentityDB、Identity API 三个资源,配合
AutoAuthorizeMiddleware在免登录前提下验证下单、草稿、取消、发货全流程; - 务必先启动 Docker:这是 tests/README.md 唯一明确强调的硬性前置条件,功能测试的一切容器编排都建立在其之上。
综上,eShop 的测试体系是一套"单元测试保逻辑、功能测试保链路"的双层方案:单元测试层以 MSTest + NSubstitute 将服务与领域逻辑打磨到可快速回归,功能测试层则以 .NET Aspire 为底座,把真实数据库容器编排进测试进程,为电商核心 API 提供了接近生产环境的端到端验证能力。
- 后端
- 电商
- 前端
- 微服务
【免费下载链接】eShop
A reference .NET application implementing an eCommerce site
相关推荐
OGX 测试体系深度解析:Record-Replay 集成测试与单元测试实战指南
OGX 测试体系深度解析:Record Replay 集成测试与单元测试实战指南 OGX(Open GenAI Stack)作为面向多 Provider 的 G
AI应用API网关后端模型推理服务Nix 测试体系完全指南:从单元测试、功能测试到模糊测试与安装器测试的实战解析
Nix 测试体系完全指南:从单元测试、功能测试到模糊测试与安装器测试的实战解析 Nix 是纯函数式包管理器,其正确性高度依赖一套分层严密的测试体系。本文以 Ni
包管理器开发工具CLI构建工具MoneyPrinterTurbo:5分钟从创意到短视频的AI自动化神器
MoneyPrinterTurbo:5分钟从创意到短视频的AI自动化神器 还在为制作短视频而烦恼吗?从文案构思到素材剪辑,再到配音字幕,传统视频创作流程不仅耗时
AI 应用媒体生成音视频视频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考