news 2026/10/2 8:05:57

eShop .NET 电商参考实现测试体系深度指南:单元测试、功能测试与 Aspire 测试容器实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
eShop .NET 电商参考实现测试体系深度指南:单元测试、功能测试与 Aspire 测试容器实战
  • 后端
  • 电商
  • 前端
  • 微服务

【免费下载链接】eShop

A reference .NET application implementing an eCommerce site

项目地址:https://gitcode.com/GitHub_Trending/es/eShop
点击查看免费下载

本指南以 tests/README.md 为骨架,系统梳理 eShop 参考实现(.NET 电商站点)中用于验证各业务组件行为的测试体系。仓库tests/目录下同时包含单元测试与功能测试两类工程:前者以 MSTest + NSubstitute 对服务与领域逻辑做快速隔离验证,后者借助.NET Aspire 托管测试容器(如 PostgreSQL)在真实依赖环境下端到端验证 API 行为。读完本文,你将掌握 eShop 各测试项目的结构、核心测试手法、Aspire 测试基础设施的搭建方式,以及运行这些测试所需的前置条件(Docker)。

一、测试体系总览:单元测试与功能测试的分工

原文档开宗明义地指出:tests/目录存放一组用于验证 eShop 应用各组件行为的单元测试(unit tests)与功能测试(functional tests)。结合仓库实际布局,这两个层次的分工非常清晰:

层次测试项目验证对象依赖
单元测试Basket.UnitTestsBasket.API 的 gRPC 服务逻辑NSubstitute Mock + 无外部依赖
单元测试Ordering.UnitTestsOrdering 领域模型与命令处理器NSubstitute Mock + 领域模型
单元测试ClientApp.UnitTestsMAUI 客户端服务层内置 Mock 服务
功能测试Catalog.FunctionalTestsCatalog.API 的 REST API 全流程Aspire + PostgreSQL 测试容器
功能测试Ordering.FunctionalTestsOrdering.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(); } ... }

这段代码揭示了三个关键设计:

  1. 测试即 Aspire 编排:测试进程本身就是一个简化版 Aspire 应用,通过AddPostgres("CatalogDB")声明 PostgreSQL 资源,Aspire 会负责在 Docker 中拉起容器并管理其生命周期;
  2. 选用 pgvector 镜像:Catalog 功能测试使用的镜像为ankane/pgvector,这与 Catalog.API 中商品目录的语义化检索(semantic search)能力相匹配——测试容器需要具备向量存储扩展;
  3. 关闭仪表盘: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
GetCatalogItemsbyIdsids=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。因此:

  1. Docker 必须正在运行——这是 tests/README.md 明确声明的前置条件,否则_app.StartAsync()阶段将因无法创建容器而失败;
  2. 首次运行会拉取镜像,取决于网络环境可能需要一定时间;Catalog 功能测试使用WithImageTag("latest")拉取最新版ankane/pgvector镜像;
  3. 测试容器默认不共享 Dashboard(DisableDashboard = true),测试结束通过DisposeAsync依次停止并释放 Aspire host 与被测应用。

运行单个功能测试项目:

dotnet test tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj

5.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

项目地址:https://gitcode.com/GitHub_Trending/es/eShop
点击查看免费下载

相关推荐

上一篇:网络安全扫描工具Nuclei配置与使用完全指南
下一篇:HeyGem.ai本地部署完整指南:3条命令跑通全离线数字人视频合成环境

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 8:05:46

Redis如何通过MCP协议成为AI Agent协作节点

1. “Redis 已正式接入 AI&#xff01;”——这不是营销话术&#xff0c;而是架构层的真实演进你刷到这个标题时&#xff0c;第一反应可能是&#xff1a;又一个蹭AI热度的PR稿&#xff1f;Redis不是那个内存数据库吗&#xff1f;它跟大模型、智能体、推理链路有什么关系&#x…

作者头像 李华
网站建设 2026/10/2 8:05:04

Remote In Tech 公司档案解析:Exoscale 的完全远程欧洲云托管实践

数据集 【免费下载链接】remote-jobs Source for remoteintech.company — a community-maintained directory of remote-friendly tech companies 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/re/remote-jobs 点击查看 免费下载 这篇技术指南以 exoscale.md 这…

作者头像 李华
网站建设 2026/10/2 8:04:46

在 Linux/Raspberry Pi 上使用 RF24 Python 封装:安装、配置与实战

嵌入式硬件开发 【免费下载链接】ESP32-Bit-Pirate A Hardware Hacking Tool with Web-Based CLI That Speaks Every Protocol 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/es/ESP32-Bit-Pirate 点击查看 免费下载 导读 本指南以 RF24 库自带的 Python 封装文…

作者头像 李华