authentik WebUI 测试体系完全指南:unit / browser / lit / blueprints 四种测试类型的选择、编写与运行
【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik
导读
本文以 web/test/AGENTS.md 为骨架,系统讲解 authentik 前端(WebUI)自动化测试体系的组织方式:test/unit(纯 Node 单元测试)、test/browser(驱动真实 UI 的浏览器端到端测试)、test/lit(Lit 组件渲染辅助)与test/blueprints(测试种子蓝图)四种形态如何划分边界、如何取舍、如何编写与运行。结合仓库中 web/test/unit/AGENTS.md、web/test/browser/AGENTS.md 以及web/e2e/、web/test/下的真实源码与测试用例,你将掌握为 authentik WebUI 编写高质量自动化测试的完整规范,并能直接用文中命令跑通本地测试链路。
一、测试目录路由器:四种测试形态一览
authentik WebUI 是采用 Lit Web Components 与 PatternFly 4 设计系统的 TypeScript 单体仓库,包含 Flow(/if/flow/)、User(/if/user/)、Admin(/if/admin/)三个独立应用(详见 web/AGENTS.md)。与之匹配的自动化测试体系并不只有一层,而是四种各有其用途、各自有独立规范文档的形态。web/test/AGENTS.md正是这张"目录路由表"——在编写或修改任何测试之前,必须先阅读对应目录的规范文档。
| 目录 | 存放内容 | 运行器 / 环境 | 约定文档 |
|---|---|---|---|
test/unit/ | 纯 Node 测试:函数、类、模块,无 DOM 依赖 | Vitest,Node 环境 | web/test/unit/AGENTS.md |
test/browser/ | 端到端测试:在 Chromium 中驱动 admin 与 user UI 访问运行中的 authentik 实例 | Vitest browser provider(Playwright)+#e2efixtures | web/test/browser/AGENTS.md |
test/lit/ | 共享的 Lit 渲染辅助(renderLit、LitViteContext),用于组件级浏览器测试,不直接存放测试 | — | — |
test/blueprints/ | YAML 蓝图(如test-admin-user.yaml),为浏览器测试提供认证所需的数据种子 | — | — |
从当前仓库目录结构可以确认这套体系已经完整落地:
- web/test/unit/ 下已有
lexer.test.ts、flow-graph.test.ts、flow-messages.test.ts、authenticator-validate-challenge-selection.test.ts、event-search.test.ts、labels.test.ts、unescape-locale-entities.test.ts以及maps/子目录下的bands.test.ts、wedges.test.ts、hexworld-style.test.ts等纯逻辑测试; - web/test/browser/ 下则是一组按编号与功能域组织的端到端套件:
100-session.test.ts、200-modals.test.ts、300-users.test.ts、400-groups.test.ts、500-roles.test.ts、600-providers.test.ts、700-applications.test.ts、800-rac.test.ts、900-invitations.test.ts以及会话生命周期、passkey、路由基路径等专项套件; - web/test/lit/ 提供
rendering.js与setup.js两个渲染辅助文件; - web/test/blueprints/test-admin-user.yaml 是唯一的种子蓝图,定义测试管理员账号。
web/test/CLAUDE.md、web/test/unit/CLAUDE.md、web/test/browser/CLAUDE.md仅以一行@AGENTS.md引用各自的规范文档,进一步印证了"规范入口唯一"的设计意图。
二、如何选择正确的测试形态:自上而下的决策清单
web/test/AGENTS.md给出了一条必须从上到下逐条匹配、命中即止的决策路径,这是整个测试体系最重要的心智模型:
- 纯函数、无 DOM、无网络?→
test/unit/。便宜、快速、适合高分支覆盖率。具体约定见 web/test/unit/AGENTS.md。 - 用户真实点击的功能流程(向导、对话框、导航、列表表格、登录)? →
test/browser/。驱动真实 UI;禁止用@goauthentik/api客户端写单元测试来"假装"覆盖 UI 流程。约定见 web/test/browser/AGENTS.md。 - 针对某个具体 Bug 的回归?找到
test/browser/中它所属的功能套件,在现有文件中追加一个test(...)用例。不要新建以 Bug 命名的文件。如果 Bug 出在纯函数中,则往匹配的test/unit/文件里追加it(...)。 - Lit 组件在隔离环境中的行为(组件生命周期、slots、事件、响应式更新,无整应用上下文)? → 在源码旁放置
Component.browser.test.ts——Vitest 配置会匹配**/*.browser.test.ts,而 web/test/lit/setup.js 会向page暴露renderLit(...)用于挂载组件。由于目前还没有消费者,新增第一个用例前需要与团队确认。
如果你发现自己想做一件无法干净落入上述任何一个桶的事——例如写一个导入了 Lit 组件的单元测试,或者写一个通过 REST API 播种数据的浏览器测试——那正是"选错桶"的强烈信号,请回头重读目标桶的规范文档。
从仓库现状看,第 4 条已经出现了一个先行实践:web/test/component/ak-map.browser.test.ts 以ak-map.browser.test.ts的命名方式紧挨源码组件所在目录存放,印证了"colocate 组件级浏览器测试"的落地形态。
三、跨目录通用规则:所有测试都必须遵守
无论落在哪个桶,以下规则适用于test/下的一切代码:
- 禁止自定义 API 客户端:绝不要在测试文件内构建基于
fetch的管理端客户端。单元测试不需要它;浏览器测试必须驱动 UI;如果确实存在数据播种缺口,应扩展 fixture 或 blueprint 而不是写客户端。 - 禁止硬编码凭据(fixture 中已有的除外):浏览器测试通过
session.login()认证,使用 web/test/blueprints/test-admin-user.yaml 中的 bootstrap 管理员。不得在测试中读取process.env.AK_TEST_BOOTSTRAP_TOKEN。 - 实体命名确定性:浏览器测试创建数据时,必须用
IDGenerator.randomID(...)保证唯一性(详见浏览器规范);单元测试永远不需要它。 - 一个文件对应一个功能 / 符号:抵制创建以 Bug、工单号或日期命名的一次性文件。
- 测试名称必须是完整句子:如
"returns null once the input is exhausted"、"Create application with existing provider"。不能是"works",更不能是"#22383"。
其中"确定性命名"在源码中有清晰的落点:randomName与IDGenerator由 web/e2e/utils/generators.ts 提供。其实现用种子字符串做 32 位简单哈希(hash * 31 + charCode),再对形容词、颜色字典长度取模,从而把同一seed稳定映射到同一组短语,再经capitalCase生成形如 "Amber Quiet Falcon" 的可读名称。
四、运行方式:一条命令到单文件过滤
npm test # 同时运行两个项目(unit + 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 源码)浏览器测试要求有一个可运行的 authentik 实例,地址由AK_TEST_RUNNER_PAGE_URL指定(默认http://localhost:9000)。如果实例未就绪,web/test/browser/prerequisites.setup.ts 中的健康检查会立即报错退出。
单元测试的快速迭代则另有更细的过滤手段(见 web/test/unit/AGENTS.md):
npx vitest run test/unit/lexer.test.ts # 单文件 npx vitest test/unit/lexer.test.ts -t "tokenization" # 按名称过滤npm test脚本同时驱动 unit 与 browser 两个 Vitest 项目;纯逻辑改动建议直接跑单文件以获得最快反馈。npm run test:e2e走 web/playwright.config.js 配置的 Playwright CLI 路径,对 Chromium 配置了"首次重试时记录 trace"与深色配色方案。
五、单元测试(test/unit):纯逻辑的快速验证层
5.1 定位与适用场景
单元测试是纯 Node、无浏览器的测试:针对独立函数、纯逻辑和无 DOM 依赖的模块,运行在 Vitest 的 Node 环境下——没有 Playwright、没有 Lit 渲染、没有运行中的 authentik 实例。适合:
- 被测对象是普通函数或类,无 DOM、无网络、无组件生命周期;
- 想全面而快速地覆盖分支、边界、错误路径与不变式;
- 给定输入行为确定——没有定时器、没有外部服务、没有
customElements.define。
一旦答案涉及渲染 Lit 组件、点击、等待网络或对 DOM 断言,就不属于这里,应推给 colocate 的 Lit 组件测试或test/browser/。
5.2 文件布局与导入
- 文件位于
test/unit/*.test.ts,一个文件对应一个被测模块/特性,以符号或模块命名(如lexer.test.ts); - Vitest 配置同时匹配全工作区的
**/*.unit.test.ts,因此紧密耦合的测试也可以作为foo.unit.test.ts放在源码旁边,比塞进平行的test/unit/目录更清晰; - 必须从
vitest导入describe/it/expect/vi,严禁从#e2e导入test/expect(那是浏览器测试专用的,会拖入 Playwright); - 通过包别名
#flow/…、#elements/…、#common/…访问源码,绝不使用指向src/的相对路径。这与 web/AGENTS.md 中"导入优先使用package.json#imports定义的别名"的整体约定一致,确保测试从任何位置(包括测试目录)都能正确解析导入; vi用于 spy、mock 与定时器;优先使用真实实现,只在真正成问题的模块边界(网络、时间、随机性)处打桩。
5.3 测试结构与断言惯例
import { describe, expect, it, vi } from "vitest"; import { shouldResetSelectedChallenge } from "#flow/stages/authenticator_validate/challenge-selection"; describe("shouldResetSelectedChallenge", () => { it("returns true when the previously selected challenge is no longer allowed", () => { const selected = makeDeviceChallenge(DeviceClassesEnum.Email, "email-1"); const allowed = [ makeDeviceChallenge(DeviceClassesEnum.Totp, "totp-1"), makeDeviceChallenge(DeviceClassesEnum.Webauthn, "webauthn-1"), ]; expect(shouldResetSelectedChallenge(selected, allowed)).toBe(true); }); it("returns false when the previously selected challenge is still allowed", () => { ... }); it("returns false when there was no selected challenge", () => { ... }); });要点:
- 顶层
describe(symbolName),可按方法或行为再嵌套(参考 web/test/unit/lexer.test.ts 中的describe("tokenization")等写法); it("returns X when Y")——以动词开头的完整句子,同时写明结果与前置条件;- 采用 Arrange / act / assert 三阶段,阶段之间用空行分隔;重复的测试数据形状用内联工厂(如
makeDeviceChallenge(...)),放在文件顶部,直到两个文件需要同一形状才抽到共享 helper; - 每个
it只测一个概念,一旦名字里出现 "and" 就该拆分; - 单元测试中
expect()不加断言消息——测试名与匹配器已足以表达意图,Vitest 输出足够清晰。
匹配器选型:toBe用于原始值与引用同一性,toEqual用于结构相等,toThrow(/regex/)用于错误路径(匹配消息的稳定片段而非整句),.mock.calls[i]?.[j]精确断言 spy 参数。mocking 上优先用vi.fn()内联构造测试替身而非模块级vi.mock(...);只有被测代码真正读时钟时才用vi.useFakeTimers();确实需要vi.mock("module")时提升到文件顶部并在理由不明显处加一行注释说明"为什么"。
5.4 单元测试的禁区
- 不从
@playwright/test或#e2e导入(浏览器测试专属); - 不调用
customElements.define、不导入 Lit 组件(Node 环境无 DOM,组件覆盖应交给test/browser/或 colocate 的.browser.test.ts); - 不访问网络或文件系统——纯函数测试;如果单元需要 IO,说明测错了层级;
- 不用
try/catch静默吞错——错误路径用expect(() => …).toThrow(...),缺失的 throw 必须让测试失败; - 不对快照断言,除非输出是稳定且有意为之的产物(例如 token 流)——快照在替代对契约的思考时腐烂得很快。
六、浏览器端到端测试(test/browser):驱动真实 UI
6.1 核心哲学:驱动 UI,而非 API
浏览器测试是运行在 Vitest browser runner(Chromium)下的 Playwright 测试,针对运行中的 authentik 实例端到端地演练 admin 与 user UI。其哲学有三条,决定了整个目录的写作风格:
- 驱动 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清理块,它会掩盖断言失败并吞掉真实故障。
6.2 导入方式与 fixture 体系
测试从#e2e别名导入,从不直接导入@playwright/test:
import { 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 的expect,并导出一个经 fixtures 扩展的test——它基于base.extend<E2EFixturesTestScope, E2EWorkerScope>(...)注册了navigator、session、form、pointer、passkey五类 fixture,每个测试运行时都会按需构造。
从测试回调中解构你需要的 fixture,全部按测试逐一构造:
| Fixture | 用途 |
|---|---|
session | login({ to, username?, password?, rememberMe? })、toLoginPage()、checkAuthenticated()。默认使用test-admin@goauthentik.io/test-runner(即 blueprint 中播种的 bootstrap 管理员) |
navigator | navigate(to)与waitForPathname(to)——优先于page.goto,保证 URL 等待的一致性 |
form | fill(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与 search-select 下拉 |
pointer | click(name, role?, ctx?)——按可访问名称的高层点击,默认按钮/链接 |
page | 原始 PlaywrightPage,用于 fixture 覆盖不到的地方;Shadow DOM 自动穿透 |
baseURL | 实例 URL,来自AK_TEST_RUNNER_PAGE_URL(默认http://localhost:9000) |
大多数步骤应经由form与pointer完成;只有不存在对应 fixture 方法时才使用page.locator(...)。
SessionFixture(web/e2e/fixtures/SessionFixture.ts)给出了"驱动 UI 登录"的源码级细节:它定位ak-stage-identification、按getByLabel("Username")找用户名框、getByLabel("Password", { exact: true })精确匹配密码框(避免与"Show password"开关的 substring 冲突)、getByRole("checkbox", { name: "Remember me on this device" })处理记住我,登录流程路径固定为/if/flow/default-authentication-flow/,并对认证失败用getByRole("alert", …)捕获。整套选择器策略正是规范中"ARIA role 优先、精确匹配防歧义"的实践范本。
6.3 测试结构与test.step分段
test.describe("Feature name", () => { const names = new Map<string, 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 报告中,让失败自动定位; - 名称用模块级
Map按testId缓存,在beforeEach中填充; series([fn, ...args], ...)以自上而下可读的用户动作脚本形式编排有序表单填写序列;- 对话框定位器只捕获一次,再作为
ctx?参数传入其内部的fill/click/selectSearchValue以限定作用域; - 每个
expect都要带第二个参数作为消息,以被断言的属性来措辞("Wizard opens"、"Provider is visible"),而不是复述匹配器; - 第一个参数必须是解构模式,即使不引用任何 fixture 也要写
async ({ page: _page }, { testId }) => {…}——裸标识符会触发 Playwright 运行时的First argument must use the object destructuring pattern(它靠解析参数模式决定注入哪些 fixture),而空解构async ({}, { testId }) => {…}又会触发 ESLint 的no-empty-pattern;解构并重命名是唯一同时满足两者的形式。
真实的 web/test/browser/700-applications.test.ts 完整复刻了这一骨架:test.describe("Applications")+ 模块级providerNamesMap +beforeEach中用IDGenerator.randomID(6)播种、randomName(seed)生成可读名称,随后按test.step依次"创建 OAuth2 provider""验证 provider 创建""导航到 applications""创建 application",全程只通过对话框、按钮、表单与 search 交互。
6.4 定位器优先级与断言
定位器按以下顺序选用:
- ARIA role 查询:
page.getByRole("button", { name: "Create" })、page.getByRole("dialog", { name: /Launch Endpoint/i })、page.getByLabel("Username")——能抗住样式/标记变更并表达意图; - Web 组件标签:
page.locator("ak-stage-identification")、page.locator("ak-form-group", { hasText: /Advanced/ })——稳定的元素契约; data-test-id:page.getByTestId("..."),Playwright 配置了testIdAttribute: "data-test-id",仅当 role/label 无法区分时才新增测试 id;- CSS 选择器:最后手段。
Shadow DOM 是透明穿透的——不要写.shadowRoot遍历。断言时始终传消息;只有默认 5 秒确实不够时才显式{ timeout: ... }(通常是对话框挂载或导航这类异步 UI 转换后的首个断言);不要加page.waitForTimeout,等待你真正关心的定位器条件。
6.5 反模式清单
- 测试文件内的自定义 API 客户端:无
makeAPIClient,无fetch(${baseURL}/api/v3/...)做 setup; - 从测试中读取
process.env.AK_TEST_BOOTSTRAP_TOKEN——测试应通过session.login()以真实用户身份认证; - 针对单一 Bug 的一次性回归文件——改为在相关功能套件中追加
test(...)用例; try/finally清理块——名称已随机化,让实体自然累积;- 无等待的
page.goto——用navigator.navigate(to)或session.login({ to }); - 存在 role/label 时仍对 CSS 选择器断言;
- 跳过
test.step——冗长扁平的测试难以调试,每个阶段都要包裹。
6.6 新增覆盖的路径
扩展既有套件时,跟随周边模式——同样的 fixture 解构、同样的Map<testId, name>风格、同样的"对话框即上下文"惯用法。引入新套件时以applications.test.ts或providers.test.ts为规范范例建模。如果需要尚不存在的 helper(新的表单输入形态、新的通用导航),应扩展 web/e2e/fixtures/ 中的 fixture,而不是在测试里复制逻辑。
七、Lit 组件渲染辅助(test/lit):组件级浏览器测试的基础设施
test/lit/不直接存放测试,而是提供两个共享渲染辅助:web/test/lit/rendering.js 导出LitViteContext(含render与cleanup),web/test/lit/setup.js 在 Vitest 的 browser 环境下把渲染能力挂到页面对象上:
import { 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());也就是说,任何 colocate 的*.browser.test.ts都能通过page.renderLit(...)挂载组件,且每个用例前自动清理上一次渲染的 DOM。这正好支撑了"决策清单第 4 条"的落地——目前仓库中已出现一个实践样本 web/test/component/ak-map.browser.test.ts,但尚无更多消费者,因此新增第一个组件级用例前需要与团队确认方向。
八、测试种子蓝图(test/blueprints):浏览器测试的认证基石
web/test/blueprints/test-admin-user.yaml 是一个 authentik 蓝图(blueprint)格式的 YAML 文件,为浏览器测试播种 bootstrap 管理员:
version: 1 entries: - attrs: email: test-admin@goauthentik.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它定义了用户名akadmin、邮箱test-admin@goauthentik.io、密码test-runner,并把该用户通过!Find表达式挂到authentik Admins组——这正是SessionFixture中GOOD_USERNAME/GOOD_PASSWORD(web/e2e/fixtures/SessionFixture.ts)与session.login()默认凭据的数据来源。当浏览器测试遇到"需要先有某种前置数据"的缺口时,正确做法是扩展这样的蓝图或 fixture,而不是在测试里硬编码凭据或调用 API。
九、文件与功能速查
按 web/test/AGENTS.md 的 "Where things live" 一节,各功能部件的最终落点汇总如下:
- Playwright fixtures(
session、navigator、form、pointer、passkey)与#e2e入口: web/e2e/index.ts 及 web/e2e/fixtures/(SessionFixture.ts、NavigatorFixture.ts、FormFixture.ts、PointerFixture.ts、PasskeyFixture.ts、PageFixture.ts); - Lit 渲染辅助: web/test/lit/setup.js 与 web/test/lit/rendering.js;
- 种子蓝图(测试管理员等): web/test/blueprints/test-admin-user.yaml;
- 生成器(
IDGenerator、randomName): web/e2e/utils/generators.ts 与@goauthentik/core/id(字典文件位于web/e2e/utils/dictionaries/下的adjectives.txt、colors.txt); - 浏览器测试健康检查(负责在实例未就绪时"响亮地失败"): web/test/browser/prerequisites.setup.ts。
十、总结:一张路由表驱动整个测试体系
authentik WebUI 的测试体系本质上是一个**"按形态路由"**的体系:web/test/AGENTS.md是入口路由表,unit与browser两份规范文档分别是纯逻辑层与 UI 行为层的"宪法",lit与blueprints是支撑组件级测试与认证前置的基础设施,e2e/则承载所有可复用的 fixture 与生成器。
判断一个测试该写在哪里的口诀可以浓缩为:有 DOM 交互去browser,纯逻辑去unit,组件隔离行为 colocate 成.browser.test.ts,数据缺口补 blueprint,能力缺口扩 fixture,永远不要在自己的测试文件里发明 API 客户端。遵循这张路由表与两条规范文档,你写出的测试将天然具备确定性命名、可自定位的test.step分段、消息完备的断言,以及"驱动 UI 而非 API"的回归保障能力。
【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考