拓冰建站拓冰建站
首页 / 资讯中心 / 正文

authentik WebUI 测试体系实战指南:Unit / Browser / Lit 三层测试架构与规范全解析

authentik WebUI 测试体系实战指南Unit / Browser / Lit 三层测试架构与规范全解析【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentikauthentik 的 Web 前端是一个基于 Lit Web Components 与 PatternFly 4 的 TypeScript monorepo包含 Flow/if/flow/、User/if/user/、Admin/if/admin/三个独立 UI 应用。为了在不同抽象层级上持续保障这些界面的质量仓库在 web/test 目录下建立了一套分层测试体系。本文以 web/test/AGENTS.md 及其子目录约定文档为主体结合真实测试源码web/test/browser/*.test.ts、web/test/unit/*.test.ts、Playwright fixturesweb/e2e/与种子蓝图web/test/blueprints/完整讲解这套体系的目录结构、层级选择决策、跨层通用规则、fixtures 用法与测试编写规范帮助你在为 authentik WebUI 贡献测试时写出符合项目惯例、可稳定运行、易于排查失败的高质量用例。一、测试体系的整体设计三个目录三种运行环境web/test/是 authentik WebUI 的测试目录路由器。它并不直接存放测试实现而是把三类不同运行环境的自动化测试组织到三个子目录中每个子目录都有自己独立的约定文档目录存放内容运行环境 / Runner约定文档test/unit/无 DOM 依赖的函数、类、模块的纯 Node 测试VitestNode 环境test/unit/AGENTS.mdtest/browser/在 Chromium 中驱动 Admin / User UI、面向运行中的 authentik 实例的端到端测试Vitest browser provider基于 Playwright#e2efixturestest/browser/AGENTS.mdtest/lit/组件级浏览器测试共用的 Lit 渲染辅助renderLit、LitViteContext本身不直接存放测试——test/blueprints/注入 authentik 的 YAML 蓝图如test-admin-user.yaml供浏览器测试登录认证使用——这套结构的关键设计意图是按依赖面而非按功能划分测试。纯逻辑测试不引入浏览器开销可快速覆盖分支与边界UI 流程测试则严格走真实界面路径确保回归能被完整捕获。从 CLAUDE.md 到 AGENTS.md文档的路由机制web/test/CLAUDE.md本身只有一行AGENTS.md这是 Claude Code 的文件包含指令表示该目录的行为约定实际定义在web/test/AGENTS.md中。web/test/browser/CLAUDE.md与web/test/unit/CLAUDE.md同样通过AGENTS.md分别指向web/test/browser/AGENTS.md和web/test/unit/AGENTS.md。因此阅读任何一份测试代码之前应先按此链找到对应的约定文档——文档路由本身就是分层体系的组成部分。二、如何选择正确的测试层级test/AGENTS.md给出了一个自顶向下的决策清单命中即停避免把测试放错层级纯函数、无 DOM、无网络→ 放入test/unit/。这类测试廉价、快速适合密集的分支覆盖详见unit/AGENTS.md。用户实际点击走通的特性流程向导、对话框、导航、列表表格、登录→ 放入test/browser/。必须驱动真实 UI不要用goauthentik/api客户端伪造一个单元测试来模拟它。针对某个具体 bug 的回归测试→ 找到它所属的特性套件feature suite在test/browser/中追加一个test(...)用例不要新建以 bug 命名的孤立文件。如果 bug 在纯函数中则在对应的test/unit/文件中追加it(...)。Lit 组件在隔离环境下的行为生命周期、slots、事件、响应式更新无整体应用上下文→ 将测试与源码放在一起命名为Component.browser.test.ts由 Vitest 配置通过**/*.browser.test.tsglob 自动拾取并用test/lit/setup.js暴露的page.renderLit(...)挂载组件。文档特别强调了一个危险信号判断法如果你发现自己在做不属于任何一个桶的事——比如在单元测试里 import 一个 Lit 组件或是在浏览器测试里直接调用 REST API 播种数据——这强烈说明你选错了层级应重新阅读目标层级的约定文档。从web/test/目录的实际文件可以看到这套决策的落地纯逻辑测试集中在 test/unit如lexer.test.ts、flow-graph.test.ts、labels.test.ts用户流程测试按特性编号分布在 test/browser100-session.test.ts、300-users.test.ts、400-groups.test.ts、500-roles.test.ts、600-providers.test.ts、700-applications.test.ts、900-invitations.test.ts等组件级测试则采用就近放置的方式如test/component/ak-map.browser.test.ts。三、跨层通用规则在test/内处处适用无论写哪一层测试以下规则都是硬性约束禁止自建 API 客户端。不要在测试文件里手写基于fetch的 admin 客户端。单元测试不需要它浏览器测试必须驱动 UI如果确有播种缺口应扩展 fixture 或蓝图。禁止硬编码超出 fixture 范围的凭据。浏览器测试通过session.login()使用 test/blueprints/test-admin-user.yaml 中定义的 bootstrap 管理员认证不要在测试里读取process.env.AK_TEST_BOOTSTRAP_TOKEN。实体命名必须确定性唯一。浏览器测试创建数据时用IDGenerator.randomID(...)保证唯一性约定见 browser conventions单元测试通常不需要。一个特性/符号对应一个文件。抵制创建以 bug、工单号或日期命名的临时文件。测试名必须是完整句子。如returns null once the input is exhausted、Create application with existing provider而不是works、#22383。四、运行方式与前置条件web/test/AGENTS.md与各子约定文档给出了完整的运行命令集均需在 web 目录下执行依赖见 web/package.json 的scripts字段npm test # 同时运行两个 Vitest projectunit browser npx vitest run test/unit # 只跑单元测试 npx vitest run test/browser # 只跑浏览器测试 npx vitest run path/to/single.test.ts # 只跑单个测试文件 npm run test:e2e # Playwright e2e CLI 路径使用相同的 test/browser 源码单元测试的快速迭代方式按名称过滤npx vitest run test/unit/lexer.test.ts # 单个文件 npx vitest test/unit/lexer.test.ts -t tokenization # 按 describe 名称过滤浏览器测试的关键前置条件需要一个运行中的 authentik 实例地址由环境变量AK_TEST_RUNNER_PAGE_URL指定默认http://localhost:9000。web/test/browser/prerequisites.setup.ts中的健康检查会在实例不可达时报错退出保证后续测试不会在错误环境上静默失败setup(Web server availability, async ({ baseURL }) { expect(baseURL, Base URL is set).toBeTruthy(); const ok await fetch(baseURL!) .then((res) res.ok) .catch(() false); expect(ok, Web server should be listening on ${baseURL}).toBeTruthy(); });除了健康检查prerequisites.setup.ts还演示了跨套件共享前置状态的正确做法101-session-lifecycle的记住登录remember-me覆盖依赖默认识别阶段上的一个开关因此在所有 worker 启动之前通过一次管理员登录把该开关打开——而不是在某个beforeAll里边保存流程阶段边与其他 worker 并发登录并发写会互相干扰。五、单元测试test/unit/规范深度解读test/unit/AGENTS.md将单元测试定义为纯 Node、无浏览器的测试针对独立函数、纯逻辑和无 DOM 依赖的模块运行在 Vitest 的 Node 环境下——不涉及 Playwright、不渲染 Lit、不依赖真实 authentik 实例。何时该用单元测试被测对象是普通函数或类无 DOM、无网络、无组件生命周期想快速且彻底地覆盖分支、边界、错误路径和不变量行为对输入是确定性的——没有定时器、没有外部服务、没有customElements.define。一旦涉及渲染 Lit 组件、点击、等待网络或断言 DOM就不属于这里应推送到就近的组件测试或test/browser/。文件布局与导入文件位于test/unit/*.test.ts一个文件对应一个被测模块/特性以符号或模块命名如lexer.test.ts、authenticator-validate-challenge-selection.test.ts。Vitest 配置还会拾取全工作区内的**/*.unit.test.ts因此紧耦合的测试可以就近放在源码旁foo.unit.test.ts。导入必须走包级#alias#flow/…、#elements/…、#common/…禁止用相对路径深入src/。这些别名在 web/package.json 的imports字段中映射。只从vitest导入describe/it/expect/vi不要从#e2e导入test/expect——那是浏览器测试专用的会拖入 Playwright。测试形态与命名test/unit/lexer.test.ts是文档反复引用的范本。它的结构是describe(Lexer)下按行为嵌套describe(addRule)、describe(setInput)等每个it用完整句子同时陈述前提与结果describe(addRule, () { it(returns the lexer for chaining, () { const lexer new Lexer(); expect(lexer.addRule(/a/, () a)).toBe(lexer); }); it(preserves multiline, ignoreCase, and unicode flags when re-compiling, () { const lexer new Lexer(() null); const seen: string[] []; lexer.addRule(/^a/im, (m) { seen.push(m); }); lexer.setInput(A\nA); drain(lexer); expect(seen).toEqual([A, A]); }); });约定要点顶层用describe(symbolName)可按方法或行为嵌套it(returns X when Y)以动词开头写完整句子同时陈述结果与前提。坏的命名works、handles nulls好的命名returns null once the input is exhausted、rolls back the lexer index when an action rejects。Arrange / act / assert 三阶段之间用空行分隔便于扫读重复的测试数据形状用文件顶部的内联工厂函数如makeDeviceChallenge(...)直到两个文件都需要时才提到共享 helpers。一个it只测一个概念如果命名时想用 and就拆分。单元测试的expect()不加断言消息——测试名与 matcher 已表达意图Vitest 的输出足够。断言与 Mock使用纯 Vitest matchertoBe基本类型与引用同一性、toEqual结构相等、toThrow(/regex/)错误路径匹配消息的稳定片段而非整句、.mock.calls[i]?.[j]精确断言 spy 参数。优先用vi.fn()内联构造测试替身而不是模块级vi.mock(...)只有被测代码真正读取时钟时才用vi.useFakeTimers()如果必须vi.mock(module)把它提升到文件顶部并在理由不明显时用一行注释说明原因。错误路径必须用expect(() …).toThrow(...)显式断言不要用静默的try/catch吞掉异常导致测试假通过。除非输出是稳定、有意的产物如 token 流否则不要断言快照——快照在代替思考契约时腐烂得很快。单元测试的禁区不 importplaywright/test或#e2e不调用customElements.define或 import Lit 组件Node 环境没有 DOM不访问网络或文件系统需要 IO 说明测错了层。六、浏览器测试test/browser/规范深度解读test/browser/AGENTS.md定义了这类测试的本质在 Vitest 的 browser runnerChromium下运行的 Playwright 测试端到端地驱动 Admin 与 User UI面向运行中的 authentik 实例。测试位于test/browser/*.test.ts支撑的 fixtures 与 helpers 位于web/e2e/。三条核心理念驱动 UI而不是驱动 API。特性测试应走用户路径点New Provider、填表单、点Create、验证它出现。不通过 REST API 播种实体然后只点一个按钮验证单一副作用。如果 UI 流程坏了测试必须跟着坏如果从 API 抄近路向导、模态框、导航、表单绑定的回归就全部漏检。覆盖特性而不是覆盖 bug。测试文件以特性命名providers.test.ts、applications.test.ts而非以 bug 命名特定缺陷的回归测试作为既有特性套件里追加的test(...)用例而非带专属 API 管线的孤立文件。无自建 HTTP 客户端。如果开始写makeAPIClient辅助函数停下来要么驱动 UI 创建前置状态要么当前置确实超出被测特性范围时扩展 fixture 使其可复用。此外还有一条务实的不做显式清理规则实体名用IDGenerator.randomID(...)播种每次运行产生唯一 slug陈旧实体不会冲突允许在开发环境中自然累积。不要加try/finally清理块——它们会遮蔽测试末尾的断言且在 UI 流程崩溃时倾向于吞掉真正的失败。#e2e入口与 fixtures测试从#e2e别名导入绝不直接 importplaywright/testimport { expect, test } from #e2e; import { randomName } from #e2e/utils/generators; import { IDGenerator } from goauthentik/core/id; import { series } from goauthentik/core/promises;#e2e入口即 web/e2e/index.ts它从playwright/test重新导出expect并通过base.extend注册自定义 fixtures。从源码可以看到每个 fixture 都是按测试test scope构造的接收page与测试标题export const test base.extendE2EFixturesTestScope, E2EWorkerScope({ navigator: async ({ page }, use, { title }) { await use(new NavigatorFixture(page, title)); }, session: async ({ page, navigator }, use, { title: testName }) { await use(new SessionFixture({ page, testName, navigator })); }, form: async ({ page }, use, { title }) { await use(new FormFixture(page, title)); }, pointer: async ({ page }, use, { title: testName }) { await use(new PointerFixture({ page, testName })); }, passkey: async ({ page, context }, use, { title: testName }) { await use(new PasskeyFixture({ page, testName, context })); }, });约定文档总结了每个 fixture 的职责Fixture用途sessionlogin({ to, username?, password?, rememberMe? })、toLoginPage()、checkAuthenticated()。默认使用test-admingoauthentik.io/test-runnernavigatornavigate(to)与waitForPathname(to)——用它们替代page.goto保证 URL 等待行为一致formfill(label, value, ctx?)、search(query, ctx?)、selectSearchValue(label, pattern, ctx?)、setInputCheck(label, bool, ctx?)、setRadio(group, name, ctx?)、setFormGroup(pattern, open, ctx?)。知晓ak-switch-input、ak-form-group与搜索选择下拉框pointerclick(name, role?, ctx?)——按可访问名称accessible name的高层点击默认按钮/链接page原始 PlaywrightPage用于 fixtures 未覆盖的场景Shadow DOM 自动穿透baseURL实例 URL来自AK_TEST_RUNNER_PAGE_URL默认http://localhost:9000session.login()的凭据对应 test/blueprints/test-admin-user.yaml 播种的 bootstrap 管理员用户名akadmin、邮箱test-admingoauthentik.io、密码test-runner隶属于authentik Admins组——测试通过真实登录流程获得会话而不是从环境变量偷 token。一个标准浏览器测试的完整形态文档给出的范本结构如下test.describe(Feature name, () { const names new Mapstring, string(); test.beforeEach(Seed names, async ({ page: _page }, { testId }) { const seed IDGenerator.randomID(6); names.set(testId, ${randomName(seed)} (${seed})); }); test(Do the thing, async ({ session, navigator, form, pointer, page }, testInfo) { const name names.get(testInfo.testId)!; const { fill, search, selectSearchValue } form; const { click } pointer; await test.step(Authenticate, async () { await session.login({ to: /if/admin/core/providers }); }); const dialog page.getByRole(dialog, { name: New Provider Wizard }); await test.step(Open wizard, async () { await expect(dialog, Wizard is initially closed).toBeHidden(); await click(New Provider); await expect(dialog, Wizard opens).toBeVisible(); }); await test.step(Fill form, async () { await series( [click, OAuth2/OpenID, option], [fill, Provider Name, name], [ selectSearchValue, Authorization Flow, /default-provider-authorization-explicit-consent/, ], [click, Create], ); }); await test.step(Verify created, async () { await expect(await search(name), Provider is visible).toBeVisible(); }); }); });其中蕴含的约定包括每个特性一个test.describe测试名用朴素的祈使句每个有意义的阶段都用test.step(...)包裹——它们会出现在 trace 与 HTML 报告中让失败自定位名字以testId为键存放在模块级Map中在beforeEach里播种series([fn, ...args], ...)用于有序的表单填写序列自上而下读起来就像一段用户操作脚本对话框 locator只捕获一次之后作为ctx?参数传给内部的fill/click/selectSearchValue以限定作用域每个expect都有第二个参数作为断言消息以被断言的属性措辞如 Wizard opens、Provider is visible而非复述 matcher第一个参数必须是解构模式即使不引用任何 fixture 也要写async ({ page: _page }, { testId }) {…}裸标识符async (_, { testId }) {…}会在运行时抛First argument must use the object destructuring patternPlaywright 靠解析参数模式决定注入哪些 fixtures而空解构async ({}, { testId }) {…}会触发 ESLint 的no-empty-pattern——解构并重命名是唯一同时满足两者的写法。Locator 优先级按顺序优先使用ARIA role 查询page.getByRole(button, { name: Create })、page.getByRole(dialog, { name: /Launch Endpoint/i })、page.getByLabel(Username)——能扛住样式/标记变化并表达意图Web Component 标签page.locator(ak-stage-identification)、page.locator(ak-form-group, { hasText: /Advanced/ })——稳定的元素契约data-test-idpage.getByTestId(...)——Playwright 配置已设置testIdAttribute: data-test-id仅在 role/label 无法区分时新增CSS 选择器最后手段。Shadow DOM 是透明穿透的——不要写.shadowRoot遍历Playwright 自动穿过。断言风格await expect(dialog, Dialog is initially closed).toBeHidden(); await expect(dialog, Dialog opens).toBeVisible(); await expect(row, Endpoint row appears without manual refresh).toBeVisible({ timeout: 5_000 }); await expect(input, Input has expected value).toHaveValue(foo); await expect(checkbox, Checkbox is checked).toBeChecked();永远带消息只有在默认 5s 确实不够时才显式指定{ timeout: ... }通常是对话框挂载或导航等异步 UI 转换后的首个断言不写page.waitForTimeout——等待你真正关心的 locator 条件即可。反模式清单测试文件里自建 API 客户端makeAPIClient、裸fetch(${baseURL}/api/v3/...)做前置从测试中读取process.env.AK_TEST_BOOTSTRAP_TOKEN应通过session.login()以真实用户身份认证为单个 bug 建独立文件的回归测试应并入相关特性套件try/finally清理块名字已随机化允许实体累积无等待的page.goto用navigator.navigate(to)或session.login({ to })存在 role/label 时仍用 CSS 选择器断言如.locator(button[typesubmit])跳过test.step又长又平的测试难以调试每个阶段都要包裹。七、Lit 组件级测试与渲染辅助当需要测试单个 Lit 组件在隔离环境下的行为生命周期、slots、事件、响应式更新且无整体应用上下文时采用就近放置的方式在组件源码旁创建Component.browser.test.tsVitest 配置通过**/*.browser.test.ts自动拾取如test/component/ak-map.browser.test.ts。挂载组件的机制在 web/test/lit/setup.jsimport { LitViteContext } from ./rendering.js; import { beforeEach } from vitest; import { page } from vitest/browser; page.extend({ // ts-expect-error Extension is not properly typed. renderLit: LitViteContext.render, [Symbol.for(vitest:component-cleanup)]: LitViteContext.cleanup, }); beforeEach(() LitViteContext.cleanup());它把LitViteContext.render扩展为page.renderLit(...)实现见 web/test/lit/rendering.js并在每个测试前后自动执行 cleanup避免组件实例泄漏。文档同时提醒目前该渲染辅助还没有真实消费者在添加第一个用例之前先与团队确认。八、种子蓝图与测试环境准备浏览器测试的认证与数据准备依赖 web/test/blueprints/test-admin-user.yaml——一个 version 1 的 authentik 蓝图播种 bootstrap 管理员version: 1 entries: - attrs: email: test-admingoauthentik.io is_active: true name: authentik Default Admin password: test-runner path: users type: internal groups: - !Find [authentik_core.group, [name, authentik Admins]] conditions: [] identifiers: username: akadmin model: authentik_core.user state: present该文件正是session.login()默认凭据test-admingoauthentik.io/test-runner的来源。如需新的播种能力规范要求扩展 fixture 或蓝图而不是在测试内自建客户端——这与跨层通用规则一脉相承。九、与 WebUI 架构的呼应这套测试体系与 web/AGENTS.md 描述的 WebUI 架构严格对应三个 UI 应用Flow / User / Admin共享 Config、CurrentTenant/Brand、SessionUser 三个核心上下文对象组件分components/依赖应用上下文与elements/可移植、不依赖上下文两层。浏览器测试中的sessionfixture 正是围绕登录会话与权限上下文展开formfixture 则感知ak-switch-input、ak-form-group等ak-前缀自定义元素——测试约定与组件前缀、Context API 的架构约束是配套设计的。架构文档还强调了一条铁律与无自建 API 客户端的测试规范互为表里绝不允许用goauthentik/api包之外的方式调用 authentik API任何情况下都不得使用 Fetch、Axios 或其他方法——测试代码同样遵守这条约束。十、总结给贡献者的落地建议在 authentik WebUI 仓库添加或修改测试时按以下顺序操作即可避免大多数返工先读目标层级的约定文档unit/AGENTS.md或browser/AGENTS.md确认被测对象属于该层纯逻辑进test/unit/或就近*.unit.test.ts用describe/it完整句子命名走#alias导入vi.fn()构造替身用户流程进test/browser/找到对应特性套件追加用例用#e2efixtures test.step 带消息的expect命名用IDGenerator.randomID(...)保证唯一组件隔离行为就近写Component.browser.test.ts用page.renderLit(...)挂载运行验证本地先npx vitest run test/unit/file快速迭代浏览器测试则确保AK_TEST_RUNNER_PAGE_URL指向可用的 authentik 实例最后用npm test全量通过。遵循这套体系测试既能覆盖真实用户路径、捕获向导与表单绑定层的回归又能以纯 Node 的速度覆盖分支边界是 authentik WebUI 长期可维护性的重要保障。【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门