资讯动态

Keystone 6 测试实战:用 getContext + node:test 为 GraphQL API 编写集成测试

发布时间:2026/9/24 16:39:01 来源:尧图企业网站定制
后端【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址https://gitcode.com/gh_mirrors/key/keystone点击查看免费下载导读本指南以仓库中的 examples/testing 示例项目为主线讲解如何为基于 GraphQL 的 Keystone 系统编写自动化测试。你将掌握getContext这一核心测试 API 的用法、如何在测试间重置 SQLite 数据库、如何用context.query/context.db操作数据以及如何通过context.withSession()模拟登录用户来验证访问控制逻辑。读完本文你可以把这套测试模式直接复用到自己的 Keystone 项目中。示例项目概览一个内置认证与访问控制的测试样板examples/testing是 Keystone 仓库中专门用于演示测试能力的示例它建立在withAuth()基于keystone-6/auth的认证包装器示例项目之上其核心目标是用最小的项目结构展示如何对 GraphQL API 编写测试。项目目录结构如下examples/testing/keystone.tsKeystone 配置入口通过createAuth(...)创建withAuth包装器配置 SQLite 数据库与无状态 sessionexamples/testing/schema.ts定义User与Task两个列表Task上带有基于 session 的过滤级访问控制examples/testing/example-test.ts全部测试用例也是本文重点讲解的对象examples/testing/prisma.config.tsPrisma 配置指定 schema 路径、迁移目录与数据库 URLexamples/testing/migrations/20260713000000_init/migration.sql初始数据库迁移examples/testing/package.json项目脚本与依赖。数据模型方面User列表包含name唯一、必填、password必填以及与Task的一对多关系Task列表包含label必填、priority枚举选择、isComplete复选框、assignedTo关联用户与finishBy时间戳。关键的访问控制逻辑位于Task列表的access.filter.update通过isAssignedUserFilter限制只有任务被指派到的用户才能更新该任务见 examples/testing/schema.ts。快速运行从零启动测试项目按照 README 中的说明先在仓库根目录安装依赖然后进入示例目录启动开发服务器# 在仓库根目录 pnpm install # 进入示例目录 cd examples/testing pnpm devpnpm dev会同时启动 Admin UI默认 http://localhost:3000与 GraphQL Playground默认 http://localhost:3000/api/graphql。你可以先在 Admin UI 中创建数据或直接在 GraphQL Playground 中手工执行查询与变更来熟悉数据结构随后再运行自动化测试。运行测试使用仓库提供的脚本见 examples/testing/package.jsontest: node --import tsx --test example-test.ts在示例目录执行pnpm test即可看到基于 Node.js 内置node:test运行器的输出末尾类似✔ Create a User using the Query API (139.404167ms) ✔ Check that trying to create user with no name (required field) fails (96.580875ms) ✔ Check access control by running updateTask as a specific user via context.withSession() (193.86275ms) ℹ tests 3 ℹ suites 0 ℹ pass 3 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 0.072292注意测试命令不需要启动开发服务器测试进程通过getContext直接构造 Keystone Context 并驱动真实的 SQLite 数据库这一点会在下文详细展开。测试基础设施getContext 与数据库重置example-test.ts的开头集中了整套测试基础设施值得逐行拆解import assert from node:assert/strict import { test, beforeEach, afterEach } from node:test import path from node:path import { resetDatabase } from keystone-6/core/testing/sqlite import { getContext } from keystone-6/core/context import config from ./keystone import * as PrismaModule from ./generated/prisma/client const migrationsDirectory path.join(__dirname, migrations) const databaseUrl process.env.DATABASE_URL || file:./keystone-example.db const context getContext(config, PrismaModule) beforeEach(async () { await resetDatabase({ filename: databaseUrl.replace(file:./, ) }, migrationsDirectory) }) afterEach(async () context.prisma.$disconnect())这里有几个关键点1.getContext是测试的核心入口。它接收 Keystone 配置对象与 Prisma Client 模块返回一个完整的KeystoneContext。从源码看getContext在 packages/core/src/lib/system.ts 中实现为先通过createSystem(config)构建系统再调用system.getKeystone(PrismaModule)拿到context并返回。这意味着测试中的 Context 与真实 HTTP 请求共享同一套列表定义、字段解析、访问控制与 hook 管线只是绕过了网络层——这正是它既能贴近真实又足够轻量的原因。2.resetDatabase保证每个测试从干净状态开始。它来自keystone-6/core/testing/sqlite其实现位于 packages/core/src/testing/sqlite.ts若非内存数据库会删除数据库文件及其-shm、-wal附属文件然后新建一个better-sqlite3实例把migrations目录下的迁移脚本逐条执行底层复用applyMigrations。也就是说每个测试用例前都会推倒重建表结构隔离性非常好。beforeEach与afterEach分别负责重置与断开 Prisma 连接避免测试之间相互污染也防止进程因连接未释放而挂起。3. 数据库 URL 可被环境变量覆盖。process.env.DATABASE_URL || file:./keystone-example.db这种写法在 keystone.ts 和 prisma.config.ts 中保持一致方便在 CI 或本地切换到其他数据库。值得一提的是keystone-6/core/testing还提供了 PostgreSQL 与 MySQL 版本的resetDatabase见 packages/core/src/testing/postgresql.ts 与 packages/core/src/testing/mysql.ts说明这套测试模式并不局限于 SQLite。测试用例拆解三层能力的逐步演示用例一用 context.query 创建数据test(Create a User using context.query, async () { const person await context.query.User.createOne({ data: { name: Alice, password: dont-use-me }, query: id name password { isSet }, }) assert.equal(person.name, Alice) assert.equal(person.password.isSet, true) })这个用例展示了context.queryAPI它是一套类型安全的 GraphQL 风格数据操作接口可以像写 GraphQL 一样通过query字符串选择返回字段。值得注意password { isSet }的写法——Keystone 的 password 字段默认不回传明文密码而是通过isSet这样的子选择暴露是否已设置这一信息这在测试认证相关逻辑时非常实用。用例二用 context.db 验证必填校验test(Check that trying to create user with no name (required field) fails, async () { await assert.rejects( async () { await context.db.User.createOne({ data: { password: not-a-password, }, }) }, { message: You provided invalid data for this operation.\n - User.name: value must not be empty, } ) })这里换用了context.dbAPI——与context.query不同context.db是更接近 ORM 风格的操作接口类似直接调用数据库层。用例验证了User.name的必填约束省略name字段会抛出包含精确错误信息的异常assert.rejects同时校验异常类型与 message 内容。对于字段级校验是否生效这类回归测试这种写法既精确又直观。用例三用 withSession 模拟用户验证访问控制第三个用例是整套示例中最有实战价值的场景——验证基于 session 的访问控制是否真的生效test(Check access control by running updateTask as a specific user via context.withSession(), async () { // seed 两个用户用于测试 const [alice, bob] await context.query.User.createMany({ data: [ { name: Alice, password: dont-use-me }, { name: Bob, password: dont-use-me }, ], query: id name, }) // 把任务指派给 Alice const task await context.query.Task.createOne({ data: { label: Experiment with Keystone, priority: high, isComplete: false, assignedTo: { connect: { id: alice.id } }, }, query: id label priority isComplete assignedTo { name }, }) // 匿名无 session更新应被拒绝 await assert.rejects( async () { await context.db.Task.updateOne({ where: { id: task.id }, data: { isComplete: true }, }) }, { message: Access denied: You cannot update that Task - it may not exist } ) // 以 Alice 的 session 更新应成功 { const result await context .withSession({ listKey: User, itemId: alice.id, data: {} }) .db.Task.updateOne({ where: { id: task.id }, data: { isComplete: true }, }) assert.equal(result.id, task.id) } // 以 Bob 的 session 更新应被拒绝 await assert.rejects( async () { await context.withSession({ listKey: User, itemId: bob.id, data: {} }).db.Task.updateOne({ where: { id: task.id }, data: { isComplete: true }, }) }, { message: Access denied: You cannot update that Task - it may not exist } ) })这个用例完整覆盖了三种身份状态匿名状态无 session 时isAssignedUserFilter直接返回false过滤条件使更新被拒绝错误信息为 Access denied: You cannot update that Task - it may not exist出于安全考虑Keystone 对不存在与无权限使用相同的提示合法用户通过context.withSession({ listKey: User, itemId: alice.id, data: {} })手工构造一个以 Alice 身份登录的 Context此时过滤条件匹配assignedTo.id alice.id更新成功非法用户换成 Bob 的 session 后过滤条件不匹配更新再次被拒绝。从源码看withSession是 Context 对象上的方法实现于 packages/core/src/lib/context/createContext.ts它基于当前 Context 构造一个新的 Context并把传入的 session 对象注入其中。这意味着你可以在不经过 HTTP 请求、不真正走登录流程的情况下模拟任意身份的会话状态这对访问控制类测试而言是极其强大的能力。Session 的结构{ listKey, itemId, data }在 schema.ts 中与认证配置保持一致也与withAuth的会话约定兼容。测试策略建议来自官方示例的实践准则README 中给出了几条值得内化为团队规范的测试建议优先聚焦高风险逻辑做单元测试。访问控制Access Control、Hooks、虚拟字段Virtual Fields、自定义 GraphQL 扩展是 Keystone 项目中业务逻辑密度最高、最容易被改坏的地方。官方建议把这些逻辑拆成纯函数直接对函数及其返回值编写单元测试——绝大多数情况下甚至不需要构造 Keystone Context测试成本更低、定位问题更快。用 getContext 做端到端/集成测试。当需要验证某个完整用例或用户流程如本示例中的指派任务→被指派人才能更新时再使用getContext编写集成测试。这样形成单元测试覆盖逻辑、集成测试覆盖流程的分层结构。不要为了测试切换数据库提供商。README 明确指出每个数据库提供商的特性略有差异测试环境与生产环境使用不同的数据库例如生产用 PostgreSQL、测试用 SQLite可能导致测试通过而生产出问题。最稳妥的做法是让测试与生产保持相同的数据库提供商。项目中的真实使用场景这套测试模式并非示例专有仓库自身的测试体系大量采用了相同 API。例如 tests/api-tests/field-groups.test.ts 中多次出现getContext(...)配合临时数据库构造测试环境的写法tests/api-tests目录下还有 hooks、access、relationships 等大量针对字段与关系行为的测试文件tests2/目录中的access.*.test.ts系列则专注于各类访问控制场景。如果你需要参考更复杂、更贴近真实业务的测试组织方式这些目录是很好的后续阅读材料。总结examples/testing示例展示了一条清晰、低成本的 Keystone 测试路径用getContext在测试进程内构造完整 Context用resetDatabase在用例之间重置数据库用context.query/context.db操作数据用context.withSession()模拟登录身份来验证访问控制。这套模式与 Node.js 内置的node:test运行器配合良好无需引入额外的测试框架即可为 GraphQL API 提供可靠的回归保障。赞分享后端【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址https://gitcode.com/gh_mirrors/key/keystone点击查看免费下载相关推荐Keystone 6 测试指南使用 keystone-6/core/testing 与 Vitest 验证 GraphQL API 行为Keystone 6 测试指南使用 keystone 6/core/testing 与 Vitest 验证 GraphQL API 行为 本指南以 Keys后端用 Jest 为 PostGraphile V5 编写数据库与 GraphQL 集成测试事务化测试模式与 Grafast 测试助手实战用 Jest 为 PostGraphile V5 编写数据库与 GraphQL 集成测试事务化测试模式与 Grafast 测试助手实战 本文是 PostGra后端API网关Keystone Admin UI 集成测试指南从 Playwright 测试编写到 CI 接入Keystone Admin UI 集成测试指南从 Playwright 测试编写到 CI 接入 本指南基于 Keystone monorepo 中的 tes后端上一篇GetQzonehistory终极指南5分钟免费备份你的QQ空间所有历史记录下一篇GetQzonehistory终极QQ空间历史数据完整备份解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价