Backstage 后端测试利器:@backstage/backend-test-utils 全面实战指南
2026/9/14 22:57:07 网站建设 项目流程

Backstage 后端测试利器:@backstage/backend-test-utils 全面实战指南

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

@backstage/backend-test-utils是 Backstage 官方为后端插件与后端应用提供的测试辅助库(Test helpers library for Backstage backends),它以极小的学习成本封装了测试数据库、服务 Mock、完整测试后端启动等能力。本文将以该包在仓库中的 README 为主体骨架,结合 backend-test-utils 源码目录 深入讲解环境变量体系、TestDatabasesmockServicesstartTestBackend等核心 API 的用法与底层实现,帮助你为 Backstage 后端插件写出可复用、可跑通、可上 CI 的高质量测试。

1. 包定位:为什么需要 backend-test-utils

在 Backstage 的新后端系统(new backend system)中,后端由大量插件(plugin)与模块(module)构成,彼此通过服务(Service)与扩展点(ExtensionPoint)解耦。要单独测试某个插件或服务工厂,开发者面临三个痛点:

  • 需要真实或近似真实的数据库实例(PostgreSQL / MySQL / SQLite);
  • 需要替身形式的认证、权限、调度、配置等核心服务;
  • 需要能够整体启动一个小型后端、用 HTTP 请求验证插件路由。

@backstage/backend-test-utils把这三件事全部封装好:数据库由 TestDatabases 自动拉起并清理,核心服务由 mockServices 提供 Mock 工厂,整机启动由 startTestBackend 一行完成。从包描述(package.json)可以看到它依赖backend-app-apibackend-defaultsbackend-plugin-apitestcontainersknexpgmysql2better-sqlite3等,这正是它既能"起容器"又能"起后端"的底气所在。

2. 安装:以 devDependency 引入

README 明确要求将本包作为开发依赖添加到你的后端插件包中。在仓库根目录下进入目标包目录并执行:

# 从 Backstage 根目录进入你的后端插件目录 cd plugins/my-plugin-backend yarn add --dev @backstage/backend-test-utils

添加后即可在测试文件中直接导入:

import { TestDatabases, mockServices, startTestBackend, mockCredentials, ServiceFactoryTester, } from '@backstage/backend-test-utils';

包的主入口 src/index.ts 统一导出了databasemswfilesystemserviceswiring五组子模块以及mockErrorHandler,同时 package.json 还暴露了一个./alpha子路径(src/alpha/index.ts),用于导入仍在演进中的 alpha 级服务 Mock(如 Actions、Metrics、Tracing 相关 Mock)。

3. 环境变量体系:控制测试行为的开关

README 用一节专门列出了测试相关环境变量,这是让同一套测试在本地、CI、离线镜像环境都能跑起来的关键。下面逐条说明并结合源码给出实现细节。

3.1 BACKSTAGE_TEST_DISABLE_DOCKER

  • 取值:设置为1时禁用所有基于 Docker 的测试。
  • 语义:用于没有 Docker 守护进程的环境(例如部分容器化 CI)。

3.2 CI

  • 取值:设置为1启用长时间运行的测试,包括依赖 Docker 的数据库与缓存测试。
  • 语义:CI 环境通常显式置1,表示"我可以接受拉镜像、起容器带来的耗时"。

这两个变量不是独立生效的,它们的组合逻辑在 isDockerDisabledForTests.ts 中实现:

return ( Boolean(process.env.BACKSTAGE_TEST_DISABLE_DOCKER) || !Boolean(process.env.CI) );

也就是说:只要显式设置BACKSTAGE_TEST_DISABLE_DOCKER=1,或者没有设置CI,Docker 测试就被视为禁用。默认情况下(本地开发、未设置CI),基于 Docker 的测试会被跳过——这也是为什么在本地跑全量数据库测试时通常要CI=1 yarn test。仓库测试代码中也直接体现了这一点,例如 postgres.test.ts 的const itIfDocker = isDockerDisabledForTests() ? it.skip : it;

3.3 BACKSTAGE_TEST_DOCKER_REGISTRY

  • 取值:Docker 镜像仓库镜像(mirror)地址,例如mycompany.docker.io/mirror
  • 作用:所有测试镜像都从这个地址拉取,适配内网/离线镜像仓库。

其实现位于 getDockerImageForName.ts:设置该变量后,镜像名会被拼成${BACKSTAGE_TEST_DOCKER_REGISTRY}/${name};未设置则原样使用。例如postgres:16在设置后会变成mycompany.docker.io/mirror/postgres:16

若镜像仓库需要认证,README 提示参考 testcontainers 的配置方式(DOCKER_AUTH_CONFIG)进行设置,测试框架会将其透传给底层容器客户端。

3.4 数据库连接字符串变量

README 列出的连接字符串变量,值应指向正在运行的数据库实例(而不是让框架去启动容器):

  • BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING
  • BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING
  • BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING
  • BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING
  • BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING

需要说明的是:README 列出的清单只到 Postgres 13,而当前仓库源码 types.ts 中的allDatabases已扩展出更多版本,包括:

测试数据库 ID驱动连接字符串环境变量默认 Docker 镜像
POSTGRES_18pgBACKSTAGE_TEST_DATABASE_POSTGRES18_CONNECTION_STRINGpostgres:18
POSTGRES_17pgBACKSTAGE_TEST_DATABASE_POSTGRES17_CONNECTION_STRINGpostgres:17
POSTGRES_16pgBACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRINGpostgres:16
POSTGRES_15pgBACKSTAGE_TEST_DATABASE_POSTGRES15_CONNECTION_STRINGpostgres:15
POSTGRES_14pgBACKSTAGE_TEST_DATABASE_POSTGRES14_CONNECTION_STRINGpostgres:14
POSTGRES_13pgBACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRINGpostgres:13
POSTGRES_12pgBACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRINGpostgres:12
POSTGRES_11pgBACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRINGpostgres:11
POSTGRES_9pgBACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRINGpostgres:9
MYSQL_8mysql2BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRINGmysql:8.4
SQLITE_3better-sqlite3无(内存数据库)

从源码看,判断某个数据库"当前环境是否可用"遵循三条规则(TestDatabases.create):

  1. 如果设置了对应的连接字符串环境变量,直接认定该数据库可用(用户自备实例);
  2. 如果该数据库不需要 Docker(如 SQLite),始终可用;
  3. 如果需要 Docker 但 Docker 被禁用(见 3.1/3.2 的组合逻辑),则不可用,测试会被过滤掉。

4. 数据库测试:TestDatabases 实战

4.1 设计理念:一个实例,多个逻辑库

TestDatabases的注释(TestDatabases.ts)说明了一个重要设计:在测试文件或describe块顶部创建一个TestDatabases实例,然后在每个用例里多次调用init。因为拉起一个"物理"数据库实例(如 Docker 容器)相当耗时,但在该实例内部创建新的逻辑数据库(CREATE DATABASE)却非常快。

import { TestDatabases } from '@backstage/backend-test-utils'; // 顶部只创建一次;create 会自动注册 afterAll 清理钩子(超时 60 秒) const dbs = TestDatabases.create(); describe.each(dbs.eachSupportedId())('my db test, %p', databaseId => { it('creates distinct databases', async () => { const db1 = await dbs.init(databaseId); const db2 = await dbs.init(databaseId); // db1 与 db2 是彼此隔离的空库,可独立建表 await db1.schema.createTable('a', table => table.string('x').primary()); await db2.schema.createTable('a', table => table.string('y').primary()); await expect(db1.select({ a: db1.raw('1') })).resolves.toEqual([{ a: 1 }]); }); });

上面这个用例与仓库自测用例 TestDatabases.test.ts 保持一致。其中eachSupportedId()会把当前环境支持的数据库 ID 逐个展开成describe.each的参数(TestDatabases.ts),这样同一段测试逻辑可以自动跑在 SQLite、Postgres、MySQL 多个后端上。

其他关键 API:

  • TestDatabases.create(options?):可传{ ids?: TestDatabaseId[]; disableDocker?: boolean }ids用于限定只测某些数据库,disableDocker默认取自isDockerDisabledForTests()的判定结果;
  • TestDatabases.setDefaults({ ids }):设置全局默认的测试数据库集合;
  • supports(id):判断某个数据库 ID 在当前环境是否可用;
  • init(id):返回一个全新的、唯一的、空逻辑库的Knex连接对象;对未知或当前环境不支持的 ID 会抛出带候选列表的明确错误(TestDatabases.ts)。

4.2 底层引擎:容器、随机库名与自动清理

TestDatabases把驱动映射到三类引擎(TestDatabases.ts):pg→ PostgresEngine,mysql2→ MysqlEngine,better-sqlite3/sqlite3→ SqliteEngine。

  • SQLite:使用:memory:内存库并通过 Knex 连接,同时开启PRAGMA foreign_keys = ON保证外键约束生效(sqlite.ts)。它不需要 Docker,任何环境都能跑,是日常单测的首选。
  • Postgres / MySQL:优先读取连接字符串环境变量并解析出连接配置;若未设置,则通过testcontainers启动容器(Postgres 暴露 5432、MySQL 暴露 3306),并为每个init调用创建随机命名的数据库(db+ 16 字节随机 hex),关闭时统一DROP DATABASE并停止容器(见 postgres.ts 与 mysql.ts)。

值得留意的是容器启动参数中的工程化细节:Postgres 容器通过PGDATA固定数据目录并配合withTmpFs使用临时文件系统,同时设置max_connections=1000以容纳并行测试;MySQL 容器则追加了--skip-log-bin--mysql-native-password=ON等参数(postgres.ts、mysql.ts)。另外 mysql.ts 的注释解释了为何不启用withReuse():复用的容器会绕过 ryuk 清理机制、在测试结束后残留,因此宁可每个 worker 各自承担内存开销也保证环境干净。

4.3 连接池配置

所有 Postgres / MySQL 测试连接统一使用 types.ts 中的TEST_POOL_CONFIG

export const TEST_POOL_CONFIG = { pool: { min: 0, max: 5, acquireTimeoutMillis: 30_000, createTimeoutMillis: 30_000, destroyTimeoutMillis: 5_000, idleTimeoutMillis: 5_000, reapIntervalMillis: 1_000, }, };

它让连接池随测试用例空闲而收缩到 0(min: 0),避免长时间占用数据库连接。旧的LARGER_POOL_CONFIG名称已被标记为@deprecated并指向同一对象。

5. 服务 Mock:mockServices 的三种用法

mockServices覆盖了 Backstage 后端核心服务:authauditorcachedatabasehttpAuthhttpRouterlifecycleloggerpermissionspermissionsRegistryrootConfigrootHealthrootLifecyclerootLoggerscheduleruserInfourlReaderevents,以及 alpha 阶段的 actions/metrics/tracing 相关 Mock(TestBackend.ts)。

源码注释(mockServices.ts)给出了三种固定使用模式:

模式一:直接调用,获得一个简化版假实现

// 函数往往接收参数来控制行为 const config = mockServices.rootConfig(); const logger = mockServices.rootLogger({ level: 'none' });

例如rootConfig()基于ObservableConfigProxy构造了一个可动态update({ data })的配置对象,初始数据通过{ data: {...} }传入(mockServices.ts);rootLogger({ level })支持'none' | 'error' | 'warn' | 'info' | 'debug'级别控制(mockServices.ts)。

模式二:调用.mock(),得到全部方法为 jest mock 的服务

const foo = mockServices.foo.mock({ someMethod: () => 'mocked result', }); // 测试后再对 mock 方法做断言 expect(foo.someMethod).toHaveBeenCalledTimes(2); expect(foo.otherMethod).toHaveBeenCalledWith(testData);

rootConfig.mock为例,其内部会把get/getBoolean/getString/has/keys等所有方法都替换为jest.fn()(mockServices.ts),你可以注入部分实现、对其余方法断言调用次数与参数。

模式三:调用.factory(options),得到可注入测试后端的 ServiceFactory

await startTestBackend({ features: [ mockServices.foo.factory({ someMethod: () => 'mocked result', }), ], });

例如给权限服务注入一个"全部拒绝"的策略:

mockServices.permissions.factory({ result: AuthorizeResult.DENY });

仓库中 CatalogPlugin.test.ts 就是这么用的:先注入AuthorizeResult.DENY的 permissions Mock,再启动 catalog 后端,随后断言匿名请求POST /api/catalog/analyze-location返回 403 且自定义 analyzer 未被调用——完整演示了 Mock 服务 + 测试后端 + HTTP 断言的组合拳。

6. 端到端式测试:startTestBackend + supertest

对于要验证真实插件路由的测试,startTestBackend(options)(TestBackend.ts)会:

  1. 解包传入的features(支持BackendFeature或 Promise 化的默认导出);
  2. 用默认的mockServices工厂集合 + 真实rootHttpRouter(监听随机端口port: 0)与基于端口的HostDiscovery构造一个特化后端;
  3. 若传入了extensionPoints元组数组([ExtensionPoint, Partial<实现>]),会自动生成注册扩展点的测试模块,让模块测试无需真实实现扩展点;
  4. 为"孤儿模块"(只有 module 没有对应 plugin 的场景)自动生成空插件占位(TestBackend.ts);
  5. 启动后端并在afterAll中统一停止所有实例(TestBackend.ts)。

返回值TestBackend暴露server属性(底层ExtendedHttpServer),可直接交给supertest发起请求:

import { startTestBackend } from '@backstage/backend-test-utils'; import request from 'supertest'; const { server } = await startTestBackend({ features: [myPlugin], }); const response = await request(server).get('/api/my-plugin/health'); expect(response.status).toBe(200);

如果rootHttpRouter被替换,server访问器会抛出明确错误(TestBackend.ts),提醒你该场景下无法再用 HTTP 方式测试。

7. 聚焦单元测试:ServiceFactoryTester 与 mockCredentials

7.1 ServiceFactoryTester:隔离测试单个服务工厂

ServiceFactoryTester 用于在隔离环境中测试单个服务工厂:ServiceFactoryTester.from(subject, options?)会以"默认 Mock 工厂 + 额外dependencies+ 被测工厂"组装一个ServiceRegistry,其中dependencies中与被测服务同名的工厂会覆盖默认实现。之后通过get()拿到被测服务的实例并执行断言。

7.2 mockCredentials:模拟三种主体身份

权限、认证相关测试离不开凭证构造。mockCredentials.ts 提供:

  • mockCredentials.none():未认证主体,配mockCredentials.none.header()可构造Bearer mock-none-token请求头;
  • mockCredentials.user('user:default/mock'):用户主体,默认实体引用为user:default/mock,也支持{ actor }指定模拟的服务方代理主体;
  • mockCredentials.service(...):服务主体凭证。

用户实体引用必须满足<kind>:<namespace>/<name>格式,否则直接抛出TypeError(mockCredentials.ts)。配合MockAuthService/MockHttpAuthService使用,即可在无需真实签发 token 的情况下覆盖认证与鉴权分支。

8. 其他辅助工具:MSW、MockDirectory 与错误处理

  • MSW 请求 MockregisterMswTestHooks(worker)(registerMswTestHooks.ts)在beforeAllworker.listen({ onUnhandledRequest: 'error' })(未处理的请求直接报错,防止"静默漏 mock")、afterEach重置 handlers、afterAll关闭 worker,用于拦截插件对 GitHub 等外部 API 的调用。
  • MockDirectory 文件系统MockDirectory(MockDirectory.ts)支持用嵌套对象或回调(回调上下文提供pathsymlink)在临时目录中构造目录内容,适合测试 scaffolder 模板、文件读写等需要"真实文件"的场景。
  • mockErrorHandler:从 util 导出的mockErrorHandler,配合app.use(mockErrorHandler())可把 express 错误中间件替换为便于断言的实现。

9. 结合仓库自测,验证使用方式

backend-test-utils自身的测试就是最好的用法范例:

  • TestDatabases.test.ts:演示"一次 create、多次 init、describe.each遍历全部支持数据库";
  • postgres.test.ts / mysql.test.ts / sqlite.test.ts:演示itIfDocker条件跳过、连接字符串优先于容器启动的引擎选择逻辑;
  • TestBackend.test.ts 与 ServiceFactoryTester.test.ts:验证后端整机启动与服务工厂隔离测试;
  • CatalogPlugin.test.ts:展示第三方插件如何用mockServices+startTestBackend+supertest组合出完整的 HTTP 级集成测试。

10. 实践建议与注意事项

  1. 本地日常开发:优先跑 SQLite(SQLITE_3无需 Docker),需要覆盖多数据库时用CI=1开启容器测试;没有 Docker 的环境显式设置BACKSTAGE_TEST_DISABLE_DOCKER=1
  2. 已有数据库实例时:设置对应版本的连接字符串变量即可让框架跳过容器启动直接连接(Postgres 用pg-connection-string解析,MySQL 用URL解析并支持?ssl=?debug=参数),注意这些库需要有创建/删除数据库的权限。
  3. 内网环境:通过BACKSTAGE_TEST_DOCKER_REGISTRY指向镜像仓库,并配置DOCKER_AUTH_CONFIG处理认证。
  4. 版本差异:README 只列出了 Postgres 9/11/12/13 与 MySQL 8 的连接字符串变量,但当前仓库源码已支持到 Postgres 18,完整清单以 types.ts 为准。
  5. 测试资源自动回收TestDatabases.create()startTestBackend()都会自动注册 Jest 的afterAll清理钩子(数据库清理超时 60 秒),一般无需手工清理;容器与临时数据库都会在测试结束时被尽力清理。

掌握这套工具链后,你可以为任何 Backstage 后端插件搭建"零基础设施依赖"的测试环境:本地秒级跑 SQLite 单测、CI 上完整覆盖 Postgres/MySQL、用 Mock 服务注入故障与权限场景、用 supertest 验证真实 HTTP 行为,从而让后端插件在合并前就得到可靠的回归保障。

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询