测试与质量
使用分层自动测试和部署门禁,让可配置平台能够安全演进。
FormPlatform 测试项目指南
English: TESTING_GUIDE.md
1. 当前测试体系
FormPlatform 使用分层验证,而不是让一种测试承担全部责任:
| 层 | 位置 | 目的 | 外部依赖 |
|---|---|---|---|
| 前端单元/契约 | `src/FormPlatform.Host/ClientApp/src/**/*.test.js` | 纯运行时逻辑、组件公共契约和回归规则 | Node.js、Vitest |
| 静态检查 | 各项目/脚本 | JSON/XML/JS/C# 结构、链接、格式和 manifest | 无或本机 SDK |
| PostgreSQL 集成测试 | `tests/FormPlatform.PostgreSql.Tests` | 真实 ORM、事务、Reference、migration/checksum | Docker |
| SQL Server 集成测试 | `tests/FormPlatform.SqlServer.Tests` | 真实 SQL Server ORM、生成主键/分页、migration/checksum | Docker |
| Playwright E2E | `tests/e2e` | 浏览器、认证、路由、表单、模块和错误 UX | 已启动 Host、Chromium、测试账号 |
| 全模块构建 | `eng/build-all.ps1` | Host/SDK/Sample/Todo/ResourceBooking 二进制兼容 | Node、.NET、同级模块仓库 |
| 人工验收 | 浏览器/数据库/日志 | Designer、打印、支付/OIDC 等环境相关流程 | 对应环境 |
当前 GitHub CI 已形成七道强制门禁:前端 Vitest、全模块构建并组装可部署 runtime、SDK/module 兼容性验证、PostgreSQL Testcontainers 集成测试、SQL Server Testcontainers 集成测试、使用独立 PostgreSQL 和一次性管理员账号启动真实 Host 的 Playwright 核心流程,以及最终的发布 runtime 完整性门禁。最终门禁依赖此前全部成功。
2. 测试前准备
基础版本:.NET 10 SDK、Node.js 22。PostgreSQL tests 需要 Docker Desktop/Engine 可运行 Linux container。E2E 需要目标 FormPlatform 已启动,且 ResourceBooking 用例需要模块已部署和客户端扩展已启用。
不要让自动测试连接开发或生产数据库。Testcontainers 会创建临时 PostgreSQL;E2E Host 也应使用专用测试数据库和秘密。
3. PostgreSQL 集成测试
项目:`tests/FormPlatform.PostgreSql.Tests/FormPlatform.PostgreSql.Tests.csproj`。
运行:
dotnet test tests/FormPlatform.PostgreSql.Tests/FormPlatform.PostgreSql.Tests.csproj -c Release
`PostgreSqlFixture` 启动 `postgres:17-alpine` container,实现 SDK 的 `IDatabaseConnectionFactory`。同一 xUnit collection 共享 container,测试通过独立表/数据或清理保证隔离。
现有覆盖:
- `DynamicRepositoryTests`:真实 provider 上的动态 Entity、CRUD、query/reference/transaction 行为;
- `ModuleMigrationTests`:migration 只记录一次、checksum 变化失败、Core/Commerce legacy schema 由 migration 建立。
3.1 增加 ORM 集成测试
测试应使用真正的 Entity Model 和 UnitOfWork,不 mock SQL dialect:
[Fact]
public async Task Query_projects_only_requested_fields()
{
var factory = Factory();
await using var work = await factory.BeginAsync("Management");
// Arrange a unique test row through the repository.
// Query with an explicit projection.
// Assert values and absence of unrequested attributes.
await work.CommitAsync();
}
每个测试使用唯一 ID/name;如果创建固定表,使用唯一表名或 class fixture 一次创建。不要依赖测试执行顺序。
3.2 SQL Server 集成测试
项目:`tests/FormPlatform.SqlServer.Tests/FormPlatform.SqlServer.Tests.csproj`。
dotnet test tests/FormPlatform.SqlServer.Tests/FormPlatform.SqlServer.Tests.csproj -c Release
`SqlServerFixture` 使用 Testcontainers 启动独立 SQL Server 2022。测试真实执行 SQL Server dialect 的 CRUD、filter、offset 分页与数据库生成主键路径,并完整应用 Core/Commerce migration 链两次,验证 checksum 保护;不连接本机安装的 SQL Server。
3.3 增加 migration 测试
至少验证:空库可执行;第二次执行不重复;历史表记录 module/id/checksum;修改同一 ID 内容会失败;新 migration 可接在旧 migration 后;三 provider SQL 在各自 CI 中最终都应实际执行。当前自动集成只覆盖 PostgreSQL,因此 MySQL/SQL Server 仍需要后续 container/job 或发布前环境测试。
3.4 Testcontainers 故障排查
- 无法连接 Docker:先运行 `docker info`。
- 拉取镜像失败:检查代理/registry;测试本身不应硬编码本机数据库。
- container 启动慢:首次镜像下载正常,CI 可使用 layer cache。
- 随机主键/表冲突:测试共享 container,使用唯一数据并清理。
- Windows volume/端口问题:当前 fixture 使用随机端口,不要自行固定 5432。
4. 发布 runtime 完整性门禁
所有前置 CI 门禁成功后,`eng/validate-release-runtime.ps1` 会检查 staged runtime:
- Host、SDK、契约程序集、appsettings、SPA 引用的静态资源、模块 manifest 与入口程序集必须齐全;
- `appsettings.json` 不得包含非空连接字符串、密码、secret、加密密钥、API key 或私钥;部署秘密只能放环境变量、User Secrets 或外部 deployment secrets 文件;
- `DataAccess:IncludeSqlParameterValues` 必须为 `false`;
- 再次调用模块二进制兼容验证,并生成列出全部交付文件 SHA-256 的 `release-manifest.json`。
在人工发布前,可在 `eng/prepare-ci-runtime.ps1` 后执行相同检查:
./eng/validate-release-runtime.ps1 -RuntimeDirectory .ci/runtime -WriteManifest
CI 的 `formplatform-release-candidate` artifact 是已测试、带完整性清单的候选发布物;分支保护应要求 `release-readiness` 成功。
5. Playwright E2E
目录:`tests/e2e`,配置默认 base URL `http://localhost:5080`,Chromium 串行执行,失败保留 trace/screenshot。
首次安装:
cd tests/e2e
npm install --no-audit --no-fund
npm run install:browsers
准备测试 Host:
1. 使用独立测试数据库和测试 secrets。
2. 应用 migrations。
3. 创建测试系统用户并分配所需角色。
4. 部署需要测试的 Todo/ResourceBooking 模块。
5. 构建客户端并启动 Host。
设置环境变量并运行:
$env:FORMPLATFORM_E2E_BASE_URL='http://localhost:5080'
$env:FORMPLATFORM_E2E_USER='e2e-admin'
$env:FORMPLATFORM_E2E_PASSWORD='use-a-secret-value'
cd tests/e2e
npm test
交互调试:
npm run test:ui
npx playwright test specs/core.spec.js --headed --debug
npx playwright show-trace test-results/<result>/trace.zip
5.1 现有核心流程
- `/form-designer` 可匿名直接打开;
- runtime 返回 Todo 与 ResourceBooking 客户端扩展;
- 登录后 Form Center 加载;
- `/resource-booking` 直接访问和刷新不被 SPA fallback 重定向;
- 服务器提交 409 后,表单仍挂载并显示统一错误。
登录用例在本地未设置账号环境变量时会 `skip`;CI 缺少账号时则直接失败,避免“绿色”结果掩盖未执行的认证流程。
5.2 编写稳定 E2E
优先使用 role/label/test id,不依赖 Tailwind class 或 DOM 层级:
test('created record navigates to its id', async ({ page }) => {
await page.goto('/forms/<form-id>')
await page.getByLabel('Name').fill('E2E item')
await page.getByRole('button', { name: /submit/i }).click()
await expect(page).toHaveURL(/\/forms\/[^/]+\/[0-9a-f-]{36}$/)
await expect(page.getByText(/saved/i)).toBeVisible()
})
只 mock 不属于当前测试目标的边界。例如测试错误 UX 时 route fulfill 409 是合理的;测试真实 ORM 保存时不要 mock submissions。每个测试自行建立/清理数据,或调用测试专用 API/fixture,不依赖前一个测试。
5.3 必须覆盖的关键矩阵
- Designer / Preview / Viewer 三模式;
- 匿名、系统用户、respondent、管理员代填;
- 新建、更新、删除、失败保留状态;
- Pagination 草稿/完成、必填 Checkbox/Radio、隐藏字段;
- DataGrid inline/modal/side pane、排序/搜索/导出;
- AsyncSelect/Tree filter 和 Reference;
- 模块直接路由、刷新、client extension 加载;
- 中英文、toast、字段错误位置;
- 打印不包含 Admin Panel;
- Survey 文件上传和授权下载。
6. 全模块构建验证
本地目录默认要求:
workspace/FormPlatform
workspace/Todo
workspace/ResourceBooking
运行:
cd FormPlatform
./eng/build-all.ps1 -Configuration Release
它依次执行 ClientApp npm ci/build、Host/SDK、Sample、Todo、ResourceBooking client/server。缺少模块目录或 manifest 会失败,不会静默跳过。
快速只验证服务器兼容时可使用 `-SkipClientBuild`;也可通过 `-TodoDirectory`、`-ResourceBookingDirectory` 指定路径。这个脚本验证编译兼容,不替代数据库或浏览器测试。
GitHub workflow 需要 repository variables:
- `FORMPLATFORM_TODO_REPOSITORY`
- `FORMPLATFORM_RESOURCE_BOOKING_REPOSITORY`
私有模块仓库另设只读 `FORMPLATFORM_MODULES_TOKEN`。任何模块未构建都应让 CI 失败。
构建 job 使用 `eng/prepare-ci-runtime.ps1` 将 Host、Vue 静态文件和三个模块整理到 `.ci/runtime`。兼容性 job 随后执行:
./eng/validate-module-compatibility.ps1 -RuntimeDirectory .ci/runtime
该命令通过 Host 的 `--validate-modules` 模式加载真实部署包,验证 manifest schema/name、模块版本与程序集版本、SDK/Platform 版本区间、SDK 引用版本,并拒绝仍引用私有 Host 程序集的旧模块。它不连接数据库,也不启动 HTTP 服务。
Playwright job 使用 `postgres:17-alpine` service,禁用与核心用例无关的大型地理数据 seed,执行 migration、系统表单和模块初始化后再运行浏览器测试。Host log、HTML report、trace、screenshot 和 PostgreSQL TRX 都以短期 artifact 保存。仓库分支保护应要求四个 job 全部成功。
7. 客户端单元、契约和静态测试
ClientApp 使用 Vitest 测试 substitution、Action Context、validation/cache/debounce、Pagination、Checkbox/Radio、DataGrid model 和内置组件契约:
cd src/FormPlatform.Host/ClientApp
npm run test:unit
npm run test:unit:watch
契约详见客户端组件契约。Vitest 测纯函数和运行时规则;控件真实交互仍优先 Playwright E2E,而不是只对实现细节做 shallow mock。生产构建继续负责 Vue template、动态 import 和 bundling。
手动静态检查还包括:`node --check` 对普通 JS、JSON/XML 解析、`git diff --check`、Markdown 相对链接检查、manifest schema/version 检查。Vue SFC 不能只靠 `node --check`,必须经过 Vite 编译或 Vue parser。
8. API 与错误契约测试
所有失败至少断言 status、content type 和稳定字段:
{
"code": "inventory.conflict",
"messageKey": "inventory.conflict",
"fallback": "...",
"fieldErrors": {},
"status": 409,
"traceId": "..."
}
500 不应包含连接字符串、SQL、密码或堆栈。401/403/404 空结果也应由 status-code middleware 补成同一协议。客户端测试应确认字段错误进入当前 editor/FormReader,而不是主页面或别的嵌套表单。
9. 手工验收
自动测试后仍需针对环境相关功能检查:OIDC callback、SMTP、PayPal/Stripe/WeChat sandbox、打印/PDF、真实浏览器 date/time、IIS/Nginx headers、大文件、生产数据库权限和备份恢复。
最小发布 smoke:健康启动无 migration/module 错误;登录/退出;Form Center;打开 Designer;新建并更新一条普通记录;提交一份 Survey;DataGrid 查询;一个模块路由;日志中无未处理异常。
10. 测试数据和安全
生产或共享环境测试账号/密码进入 CI secret,不写进 spec。当前 E2E workflow 的固定密码只属于每次 job 新建并销毁的隔离数据库,不得在任何真实环境复用。测试文件不含真实个人数据。支付/OIDC 使用 sandbox。SQL 参数日志在测试敏感流程也保持关闭,除非使用完全虚构数据。失败 artifact/trace 可能包含表单内容,设置保留期和访问权限。
11. 新功能的测试策略模板
开发前填写:
| 问题 | 答案示例 |
|---|---|
| 纯领域规则? | xUnit unit test |
| 依赖 ORM/provider? | PostgreSQL integration |
| 改 schema? | migration first/second/checksum test |
| 改 API/错误? | integration + contract assertions |
| 改 Vue 交互/路由? | Playwright |
| 改三 provider SQL? | provider-specific CI/manual environment |
| 涉及身份/权限? | allow/deny matrix |
| 涉及打印/外部服务? | manual/sandbox smoke |
修复 bug 时先建立能重现问题的测试,再修代码;验证解决后,重新评估临时 workaround 是否仍需保留。
12. 推荐 CI 阶段
1. Frontend Vitest 和组件契约;
2. ClientApp production build,以及 Host + SDK + all modules build;
3. 组装部署 runtime 并通过 Host 验证所有 SDK/module;
4. PostgreSQL integration;
5. 启动隔离 PostgreSQL/Host,运行 Playwright core;
6. 保存 runtime、TRX、Host log 和失败 traces;
7. SQL Server migration/ORM Testcontainers 集成测试;
8. 发布 runtime 完整性门禁和 SHA-256 清单;
9. staging 外部服务 smoke,批准后生产。
CI 失败时先保留最早的根因日志,不被后续级联错误淹没。构建成功不等于 migration、模块加载或浏览器流程成功。