ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Orchard Core 测试体系实战指南:xUnit v3、SiteContext 集成测试与 Playwright 端到端测试

Orchard Core 测试体系实战指南:xUnit v3、SiteContext 集成测试与 Playwright 端到端测试 CMS后端Web框架【免费下载链接】OrchardCoreOrchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.项目地址https://gitcode.com/gh_mirrors/or/OrchardCore点击查看免费下载Orchard Core 是一个基于 ASP.NET Core 的开源模块化、多租户应用框架与 CMS。本文以仓库内.agents/skills/orchardcore-unit-test/references/testing.md测试参考文档为核心骨架结合test/目录下的真实测试代码系统讲解其测试项目布局、xUnit v3 工程配置、命名规范、参数化测试、基于SiteContext的进程内集成测试夹具、Moq 模拟技巧以及 Playwright 功能测试并给出可直接复制的运行命令。读完本文你将能够按照 Orchard Core 仓库的既有约定编写单元测试与集成测试搭建租户测试夹具并用浏览器自动化验证管理端与前端功能。测试项目布局test/ 目录全景Orchard Core 的全部测试代码集中在仓库根目录的test/文件夹下每个项目承担不同层级的验证职责。依据 testing.md 的说明其布局如下test/ ├── OrchardCore.Tests/ # unit in-process integration主测试项目 │ ├── Apis/Context/ # SiteContext 测试夹具harness │ ├── Apis/ContentManagement/ # 内容 API 测试 │ ├── Apis/GraphQL/ # GraphQL 测试 │ ├── Modules/Module/ # 各模块的测试 │ ├── DisplayManagement/, Localization/, ... │ └── xunit.runner.json ├── OrchardCore.Abstractions.Tests/ # 纯单元测试 ├── OrchardCore.Tests.Integration/ # 外部服务集成测试S3 等 ├── OrchardCore.Tests.Functional/ # Playwright 浏览器自动化测试 └── OrchardCore.Benchmarks/ # BenchmarkDotNet 基准测试各项目的定位差异如下表所示项目类型职责OrchardCore.Tests单元 进程内集成主覆盖核心抽象、模块逻辑、内容 API、GraphQL、租户行为是数量最庞大、最常运行的项目OrchardCore.Abstractions.Tests纯单元测试仅针对核心抽象层如Modules/下的抽象类做纯逻辑验证不拉起宿主OrchardCore.Tests.Integration外部服务集成依赖真实外部服务如 Amazon S3、Azure Blob 等OrchardCore.Tests.Functional浏览器端到端通过 Playwright 驱动无头 Chromium验证站点安装、主题、登录、多租户等完整用户链路OrchardCore.Benchmarks基准测试使用 BenchmarkDotNet 测量性能不属于 xUnit 测试从仓库实际内容看test/OrchardCore.Tests/Apis/Context/目录下除了 SiteContext.cs 之外还包含BlogContext.cs、AgencyContext.cs、AuthenticationContext.cs、BlogPostApiControllerContext.cs、BlogPostDeploymentContext.cs、MvcTestFixture.cs、SiteStartup.cs、TablePrefixGenerator.cs等配套文件它们共同构成了一套可复用的租户测试基础设施。测试框架与工程配置xUnit v3 Microsoft Testing PlatformOrchard Core 测试采用xUnit v3通过xunit.v3.mtp-v2包接入 Microsoft Testing PlatformMTP。版本统一在仓库根目录的 Directory.Packages.props 中集中管理PackageVersion Includexunit.v3.mtp-v2 Version4.0.0 /与之配套的两个关键工程配置值得注意测试项目是Exe而不是类库。查看 OrchardCore.Tests.csproj 可以看到PropertyGroup TargetFrameworks$(CommonTargetFrameworks)/TargetFrameworks !-- Remove the underscores from member name -- OutputTypeExe/OutputType NoWarn$(NoWarn);CA1707;EnableGenerateDocumentationFile/NoWarn /PropertyGroupOutputType被显式设为Exe这是 Microsoft Testing Platform 运行器runner的要求。如果你要新增一个测试项目务必保留这个设置不要把它改成类库。关闭 shadow copy。xunit.runner.json 中的内容非常简单但含义重要{ shadowCopy: false }关闭影子复制意味着测试程序集直接从输出目录加载这在 Orchard Core 的进程内集成测试场景中是刻意为之的——测试需要加载OrchardCore.Cms.Web的 wwwroot 静态资源与模块程序集依赖真实的程序集位置。同一 csproj 还通过None Includexunit.runner.json CopyToOutputDirectoryPreserveNewest /把该配置随构建输出拷贝到产物目录保证运行器能读到它同时把src/OrchardCore.Cms.Web的NLog.config、所有*.json配置以及整个wwwroot目录链接进测试输出这正是集成测试能凭空拉起一个完整 CMS 宿主的原因之一。命名规范类名与方法名的既定约定为了让测试可读、可检索仓库对命名有明确约定见 testing.md测试类{被测主体}Tests例如Base64Tests、BlogPostApiControllerTests、JArrayTests、BlogPostTests。测试方法{动作}_{条件}_{预期结果}例如Write_WithinLimit_Succeeds、InvokeAsync_InitializedShell_SkipsSetup。从真实代码看Base64Tests中的方法遵循同样的三段式风格如DecodeToString_Default_Succeeds、DecodeToStream_Default_Succeeds。断言风格统一采用 Arrange–Act–Assert 三段式结构断言只用 xUnit 原生Assert.*仓库不使用 Shouldly 之类的断言库。一个真实的完整示例是 Base64Tests.csnamespace OrchardCore.Json.Nodes.Test; public class Base64Tests { [Theory] [InlineData(YTwOmE/, a:a?)] [InlineData(SGVsbA, Hell)] [InlineData(SGVsbG8, Hello)] [InlineData(, )] public void DecodeToString_Default_Succeeds(string source, string expected) { Assert.Equal(expected, Base64.FromUTF8Base64String(source)); } }参数化测试Theory、InlineData 与 MemberData单条用例用[Fact]需要多组输入时用[Theory]。testing.md 给出了MemberData的推荐写法——用一个静态属性返回IEnumerableobject[]作为数据源public static IEnumerableobject[] MergeArrayEntries [ [[1,2], [3], null, [1,2,3]], ]; [Theory] [MemberData(nameof(MergeArrayEntries))] public void Merge(string a, string b, JsonMergeSettings s, string expected) { ... }仓库中的真实实现位于 JArrayTests.cs它把MergeArrayHandling的三种策略Concat、Union、Replace都作为参数传入同一测试方法验证JsonArray.Merge对不同合并策略的行为差异public static IEnumerableobject[] MergeArrayEntries [ [[1, 2, 3, 4], [4, 5, 6], null, [1,2,3,4,4,5,6]], [[1, 2, 3, 4], [4, 5, 6], new JsonMergeSettings() { MergeArrayHandling MergeArrayHandling.Concat }, [1,2,3,4,4,5,6]], [[1, 2, 3, 4], [4, 5, 6], new JsonMergeSettings() { MergeArrayHandling MergeArrayHandling.Union }, [1,2,3,4,5,6]], [[1, 2, 3, 4], [4, 5, 6], new JsonMergeSettings() { MergeArrayHandling MergeArrayHandling.Replace }, [4,5,6]] ]; [Theory] [MemberData(nameof(MergeArrayEntries))] public void MergeArray_Default_RespectJsonMergeSettings( string jsonArrayContent1, string jsonArrayContent2, JsonMergeSettings mergeSettings, string expectedJsonString) { // Arrange var array JsonNode.Parse(jsonArrayContent1) as JsonArray; var content JsonNode.Parse(jsonArrayContent2); // Act var result array.Merge(content, mergeSettings); // Assert Assert.NotNull(result); Assert.Equal(expectedJsonString, result.ToJsonString()); }常用断言汇总来自 testing.md 的 Quick Reference断言用途Assert.Equal值相等断言Assert.True/False布尔断言Assert.Null/NotNull空值断言Assert.Contains集合/字符串包含断言Assert.ThrowsT/await Assert.ThrowsAsyncT同步/异步异常断言SiteContext进程内集成测试的租户夹具核心 API 面SiteContext是 Orchard Core 集成测试的中枢它会在进程内启动一个真实的 Orchard Core 宿主并按需创建一个真实租户。其定义位于 SiteContext.cstest/OrchardCore.Tests/Apis/Context/SiteContext.cs核心成员如下public class SiteContext : IDisposable { public static IShellHost ShellHost { get; } public static HttpClient DefaultTenantClient { get; } public string RecipeName { get; set; } Blog; public string DatabaseProvider { get; set; } Sqlite; public string ConnectionString { get; set; } public HttpClient Client { get; private set; } public OrchardGraphQLClient GraphQLClient { get; private set; } public virtual async Task InitializeAsync(); public async Task UsingTenantScopeAsync(FuncShellScope, Task execute, bool activateShell true); }默认值说明RecipeName默认为BlogBlog 配方DatabaseProvider默认为Sqlite每个测试都会生成一个 GUID 随机租户名与独立的表前缀测试之间互不共享状态。InitializeAsync 的工作流程从源码SiteContext.cs可以还原InitializeAsync的完整流程生成 GUID 形式的随机租户名并通过TablePrefixGenerator生成一个唯一的表前缀组装TenantApiModel包含DatabaseProvider、TablePrefix、ConnectionString、RecipeName、租户名、URL 前缀调用共享DefaultTenantClient上的POST api/tenants/create创建租户从响应中解析出租户 URL再组装SetupApiViewModelSiteName Test Site、用户名admin、密码Password01_、邮箱NickOrchard等调用POST api/tenants/setup完成站点安装并运行所选配方通过Site.CreateDefaultClient(url)创建一个绑定到该租户 URL 的HttpClient并包装成OrchardGraphQLClient供 GraphQL 测试使用。也就是说每个集成测试实际上完成了一次完整的创建租户 安装站点 运行配方的真实流程这正是它能覆盖内容 API、配方、租户行为等真实场景的原因。UsingTenantScopeAsync在租户 DI 作用域内解析服务UsingTenantScopeAsync是访问租户内部服务的唯一正确入口。其实现SiteContext.cs会先通过ShellHost.GetScopeAsync(TenantName)拿到租户的ShellScope再把HttpContextAccessor.HttpContext指向该作用域创建的HttpContext保证 Orchard Core 基于HttpContext的租户解析逻辑生效执行你的委托最后把HttpContext清空public async Task UsingTenantScopeAsync(FuncShellScope, Task execute, bool activateShell true) { // Ensure that HttpContext is not null before using a ShellScope. var shellScope await ShellHost.GetScopeAsync(TenantName); HttpContextAccessor.HttpContext shellScope.ShellContext.CreateHttpContext(); await shellScope.UsingAsync(execute, activateShell); HttpContextAccessor.HttpContext null; }典型用法是在作用域内解析ISession、IContentManager等租户服务并直接查库验证来自 testing.md 的示例await context.UsingTenantScopeAsync(async scope { var session scope.ServiceProvider.GetRequiredServiceISession(); var posts await session.QueryContentItem, ContentItemIndex(x x.ContentType BlogPost).ListAsync(); Assert.Equal(2, posts.Count()); });关键注意事项租户服务只能在UsingTenantScopeAsync内部解析外层作用域不是租户作用域拿不到租户的 DI 容器。自定义租户切换配方与数据库testing.md 给出的标准做法是子类化SiteContext并通过WithRecipe扩展方法设置配方public class AgencyContext : SiteContext { public AgencyContext() this.WithRecipe(Agency); }仓库中的 AgencyContext.cs 与此完全一致。也可以在调用InitializeAsync之前直接设置RecipeName/DatabaseProvider/ConnectionString三个属性。实际上SiteContext还提供了一组流式扩展方法SiteContext.cs可以链式组合public static T WithDatabaseProviderT(this T siteContext, string databaseProvider) where T : SiteContext; public static T WithConnectionStringT(this T siteContext, string connectionString) where T : SiteContext; public static T WithPermissionsContextT(this T siteContext, PermissionsContext permissionsContext) where T : SiteContext; public static T WithRecipeT(this T siteContext, string recipeName) where T : SiteContext;此外还有面向特定提供方的子类化模式例如LuceneContext这类provider-specific context也是通过继承SiteContext并在InitializeAsync中追加步骤实现的。仓库里的 BlogContext.cs 是一个很好的范例——它覆写InitializeAsync先执行base.InitializeAsync()再用RunRecipeAsync额外运行 Lucene 查询配方最后通过 GraphQL 查询拿到 Blog 的ContentItemId供后续断言使用public override async Task InitializeAsync() { await base.InitializeAsync(); await RunRecipeAsync(luceneRecipeName, luceneRecipePath); var result await GraphQLClient .Content .Query(blog, builder { builder.WithField(contentItemId); }); BlogContentItemId result[data][blog][0][contentItemId].ToString(); }宿主启动配置SiteStartup测试宿主的启动配置集中在 SiteStartup.cstest/OrchardCore.Tests/Apis/Context/SiteStartup.cs。它在ConfigureServices中通过AddOrchardCms配置AddSetupFeatures(OrchardCore.Tenants)启用租户管理功能使api/tenants/create、api/tenants/setup接口可用AddTenantFeatures(OrchardCore.Localization, OrchardCore.Apis.GraphQL)为每个测试租户默认启用本地化与 GraphQL 功能将YesSqlOptions.EnableThreadSafetyChecks设为true规避测试并发下的线程安全问题注册TestRecipeHarvesterTestRecipeHarvester.cs以提供测试配方注册PermissionContextAuthorizationHandler授权处理器配合PermissionsContext实现细粒度的权限模拟——测试可在请求头中携带PermissionsContext键从而以指定权限集合的身份访问受保护端点。Moq 模拟属性桩、方法设置与依赖注入对于纯单元测试仓库使用 Moq版本见 Directory.Packages.props 中的PackageVersion IncludeMoq Version4.20.72。testing.md 总结了三种常用形态// 属性桩property stub一条表达式搞定 var clock Mock.OfIClock(c c.UtcNow DateTime.UtcNow); // 完整 mockSetup 设置返回值Verify 校验调用次数 var host new MockIShellHost(); host.Setup(h h.GetSettings(Default)).Returns(settings); host.Verify(h h.GetSettings(It.IsAnystring()), Times.Once); // 注入已构建的服务用 ServiceCollection 组装 DI 容器 var sp new ServiceCollection().AddSingleton(host.Object).BuildServiceProvider();Moq 常用速查表需求写法桩一个属性Mock.OfI(x x.P v)设置一个方法m.Setup(x x.F(It.IsAnyT())).ReturnsAsync(r)校验一次调用m.Verify(x x.F(arg), Times.Once)取出模拟对象m.Object需要留意的是在 Orchard Core 的集成测试里假对象fakes往往不是手写的 double而是SiteContext的子类——也就是用真实的进程内租户来替代模拟的服务依赖这在需要验证跨组件行为时远比 Moq 更接近生产环境。Playwright 功能测试OrchardTestFixture夹具的职责与生命周期浏览器端到端测试位于test/OrchardCore.Tests.Functional/核心夹具是 OrchardTestFixture.cstest/OrchardCore.Tests.Functional/Helpers/OrchardTestFixture.cspublic sealed class OrchardTestFixture : IAsyncDisposable { public string BaseUrl { get; private set; } public IBrowser Browser { get; } public async Task InitializeAsync() { _server await OrchardTestServer.StartCmsAsync(AppDir, AppDataPath, _instanceId); BaseUrl _server.ServerAddress; _playwright await Playwright.CreateAsync(); _browser await _playwright.Chromium.LaunchAsync(new() { Headless true }); } public async TaskIPage CreatePageAsync(string traceName null) { ... } }该夹具承担三件事清理App_DataInitializeAsync会先删除App_Data_Tests{_instanceId}目录保证每次运行从干净状态开始进程内启动 CMS 服务器通过OrchardTestServer.StartCmsAsync以WebApplication.CreateBuilder方式构建并启动一个真实的 Orchard Core 宿主支持 CMS 与 MVC 两种形态BaseUrl指向动态端口管理浏览器生命周期通过Playwright.CreateAsync()启动无头 ChromiumHeadless true。测试完成后DisposeAsync会关闭浏览器、释放 Playwright、销毁服务器并清理App_Data目录先ClearAllPools清空 SQLite 连接池再删除删除失败时最多重试 15 次避免文件锁导致清理失败。编写一个功能测试testing.md 中的最小示例var page await fixture.CreatePageAsync(); await page.GotoAsync(/); await Expect(page.Locator(h1)).ToBeVisibleAsync();仓库中的真实用例 AdminMenuTreeTests.cs 展示了更完整的形态——它验证管理端菜单树SortableJS 嵌套拖拽树编辑器的拖拽重排与持久化通过稳定的data-treenode-id定位节点而非可见文本执行拖拽后等待location.reload()触发重渲染再断言节点层级变化private static async Task OpenAdminMenusTreeAsync(IPage page) { await page.GotoAndAssertOkAsync(/Admin/AdminMenu/List); await page.Locator(li.list-group-item).Filter(new LocatorFilterOptions { HasText Admin menus }) .Locator(a).Filter(new LocatorFilterOptions { HasText Edit Nodes }).ClickAsync(); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); await page.Locator(#menu).WaitForAsync(); } [Fact] public async Task AdminMenuTree_DragRootNodeIntoPlaceholder_ReparentsAndPersists() { var page await Fixture.CreatePageAsync(); await page.LoginAsync(); await OpenAdminMenusTreeAsync(page); await page.DragAsync(TreeNode(page, BlogNodeId).Locator(.menu-item-title), TreeNode(page, ContentItemsNodeId).Locator(.menu-item-title)); await page.Locator(#menu).WaitForAsync(); Assert.Equal(1, await TreeNode(page, ContentNodeId) .Locator( ol.menu-item-links li.menu-item[data-treenode-id BlogNodeId ]).CountAsync()); }追踪Tracing与相关环境变量当设置了环境变量PLAYWRIGHT_TRACING任意值即可时CreatePageAsync会为每个页面上下文开启 Playwright Tracing捕获截图Screenshots、快照Snapshots与源码Sources并在页面关闭时把 trace 压缩包写入test/OrchardCore.Tests.Functional/traces/目录文件名为trace-{测试显示名}-{序号}.zip。这套机制对排查 CI 中难以复现的浏览器侧失败非常有用。此外test/OrchardCore.Tests.Functional/README.md 还总结了以下环境变量变量作用默认值PLAYWRIGHT_TRACING启用 Playwright tracing设为任意值关闭ORCHARD_EXTERNAL跳过进程内宿主改用外部运行的应用未设置ORCHARD_URL使用ORCHARD_EXTERNAL时的基址http://localhost:5000OrchardCore__DatabaseProvider数据库提供方Postgres、MySql、SqlConnectionSQLiteOrchardCore__ConnectionString数据库连接串未设置并行隔离与日志断言功能测试项目还实现了高度的并行隔离每个测试夹具使用独立的App_Data_Tests_{FixtureName}目录共享数据库场景下每个夹具重建自己的数据库数据库环境变量在进程启动时被捕获并立即清除改为通过tenants.json和builder.Configuration下发各夹具专属连接串测试配方通过EmbeddedRecipeHarvester从嵌入资源提供避免共享文件系统状态。服务器端还通过FakeLoggerProvider捕获所有 Warning 及以上级别的日志测试收尾时调用AssertNoLoggedIssues()——只要服务器端记录了任何 Warning 级日志测试就会失败从而捕捉到浏览器里看不到的服务端问题。运行测试dotnet test 命令从仓库根目录执行testing.md 给出的标准命令# 运行整个主测试项目 dotnet test test/OrchardCore.Tests/OrchardCore.Tests.csproj # 按命名空间/类名过滤 dotnet test project --filter FullyQualifiedName~Namespace.Class例如只跑 BlogPost 相关的集成测试dotnet test test/OrchardCore.Tests/OrchardCore.Tests.csproj --filter FullyQualifiedName~BlogPost功能测试项目因为需要 Playwright 浏览器需要先构建并安装 Chromium见 test/OrchardCore.Tests.Functional/README.mddotnet build -c Release test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj dotnet exec test/OrchardCore.Tests.Functional/bin/Release/net10.0/Microsoft.Playwright.dll install chromium dotnet test --project test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-buildCI 要求所有测试必须全部通过。按照贡献规范src/docs/contributing/contributing-code.md改动 CSS/JS 时需先运行yarn build进行重构时必须用新测试来守护重构行为Refactoring is great, but if you do so, please guard it with new tests。常见坑位Gotchas综合 testing.md 与源码实现编写 Orchard Core 测试时有以下几个高频陷阱测试项目必须是ExeMTP 运行器要求OutputTypeExe新增测试项目时不要改成类库否则无法按仓库约定运行。SiteContext是IDisposable始终使用using var context new SiteContext();让租户客户端及时释放。租户服务只能在UsingTenantScopeAsync内解析外层作用域不是租户作用域拿不到ISession、IContentManager等租户服务。测试之间不得假设共享状态集成测试使用 SQLite 每个测试独立的表前缀租户名是随机 GUID配方数据以各测试自行创建为准。等待后台任务SiteContext还提供了WaitForDeferredTasksAsync最多等待 60 秒轮询ShellContext.ActiveScopes与WaitForHttpBackgroundJobsAsync最多等待 90 秒轮询HttpBackgroundJob.ActiveJobsCount在涉及延迟任务或 HTTP 后台任务的测试中需要显式等待否则可能因时序问题产生 flaky 断言。功能测试的服务器端日志AssertNoLoggedIssues()会把任何 Warning 日志视为失败调试时留意 NLog 输出。相关文档与进一步阅读testing.md本文对应的测试参考文档SiteContext 内部细节、夹具、Playwright、项目布局。src/docs/contributing/contributing-code.md仓库的代码贡献规范包含测试期望仓库没有独立的 standalone 测试指南遵循 ASP.NET Core Engineering guidelines。src/docs/getting-started/test-drive-orchard-core.md面向用户的体验 Orchard Core文档属于使用层面不用于编写测试。test/OrchardCore.Tests/主测试项目包含大量可直接参考的真实测试用例。test/OrchardCore.Tests.Functional/README.md功能测试项目的架构说明、运行方式与环境变量。AGENTS.md仓库根目录的构建命令说明。赞分享CMS后端Web框架【免费下载链接】OrchardCoreOrchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.项目地址https://gitcode.com/gh_mirrors/or/OrchardCore点击查看免费下载相关推荐OrchardCore 测试实战指南从 xUnit 单元测试到 SiteContext 集成测试与 Playwright 功能测试OrchardCore 测试实战指南从 xUnit 单元测试到 SiteContext 集成测试与 Playwright 功能测试 本篇指南以 OrchardCMS后端Web框架Automatisch测试体系Vitest单元测试与Playwright端到端测试Automatisch测试体系Vitest单元测试与Playwright端到端测试 概述 Automatisch作为开源Zapier替代方案构建了完善的测试工作流自动化后端前端低代码任务调度Jupytext JupyterLab 扩展集成测试指南基于 Playwright 与 Galata 的端到端测试体系Jupytext JupyterLab 扩展集成测试指南基于 Playwright 与 Galata 的端到端测试体系 导读 本文聚焦 JupyterLab开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表