Serverless Framework 测试体系解析:从单元测试到 AWS 集成测试环境搭建(TESTING.md 深度指南)
本文基于仓库根目录的 TESTING.md 展开,完整讲解 Serverless Framework 单仓库中单元测试与集成测试的划分、@serverlessinc/sf-core 工作区的集成测试运行方式、AWS / Serverless Dashboard / Terraform Cloud 三类测试前置资源,以及 CI 中基于 OIDC 的免长期密钥测试账号方案。读完后,你既能本地跑起任意一条集成测试套件,也能理解 MCP 强制鉴权套件为何需要预部署的 Cognito 用户池这一类"持久化前置条件"的设计。
一、测试体系总览:单元测试与集成测试的划分
仓库的测试分为两层,TESTING.md 开头的定义非常明确:
- 单元测试(Unit Tests):不依赖任何外部资源(无 AWS 账号、无 Dashboard),位于各 package 自己的 test 目录中;
- 集成测试(Integration Tests):需要预先配置好的 AWS 账号、Serverless Dashboard 组织和 Terraform Cloud 三套环境。它们在 CI 中随 Pull Request 自动运行,在满足本文档所述前置条件的前提下也可以本地运行。
主测试工作区是 packages/sf-core(npm 包名 @serverlessinc/sf-core,当前版本见 package.json 中的 version 字段)。从 jest.config.cjs 可以看到几个关键配置:
testTimeout: 600000(10 分钟/用例)——因为集成测试要真实部署 AWS 资源,默认 5 秒超时完全不够用;transform: {}关闭 babel 转换,配合脚本中统一的NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules"说明整个测试栈跑在 原生 ESM 模式(package.json中"type": "module");modulePathIgnorePatterns: ['<rootDir>/tests/python/tests/']——Python 插件的 fixture 是独立项目而非本包模块,避免 jest 把它们索引进模块图。
二、运行全部集成测试
在仓库根目录执行:
npm test -w @serverlessinc/sf-core
这条命令对应 package.json 中的 test 脚本,其完整定义是:
cross-env NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" NODE_NO_WARNINGS=1 \
jest "tests/integration/.*" \
--testPathIgnorePatterns=/node_modules/ \
--testPathIgnorePatterns=tests/integration/domains \
--testPathIgnorePatterns=tests/integration/mcp/
注意它默认排除了两个套件,这是 TESTING.md 特别强调的:
| 被排除的套件 | 专属运行方式 | 排除原因 |
|---|---|---|
domains |
npm run test:domains -w @serverlessinc/sf-core |
需要操作真实域名,脚本还额外加了 --runInBand 串行执行 |
mcp(MCP Servers) |
npm run test:mcp -w @serverlessinc/sf-core |
会部署真实 REST API,只在相关路径变更时由 CI: MCP Servers 工作流触发;其中强制鉴权子套件(mcp-auth.test.js)还依赖下文详述的 Cognito 前置池,缺失时该套件打印日志并跳过,其余部分照常运行 |
运行特定测试套件
packages/sf-core/package.json 里为每个集成目录都提供了独立脚本,按需选择即可,例如:
# 运行 resolvers 集成测试(tests/resolvers/ 目录)
npm run test:resolvers -w @serverlessinc/sf-core
# 其余常用脚本(均带 -w @serverlessinc/sf-core 前缀):
npm run test:sam # tests/integration/sam/**
npm run test:simple:nodejs # tests/integration/simple-nodejs/**
npm run test:simple:python # tests/integration/simple-python/**
npm run test:simple:dashboard # tests/integration/simple-dashboard/**
npm run test:simple:compose # tests/integration/simple-compose/**
npm run test:compose:dev # tests/integration/compose-dev/**
npm run test:compose:subset # tests/integration/compose-service-subset/**
npm run test:sandboxes # tests/integration/sandboxes/**
npm run test:esbuild # tests/integration/esbuild/**
npm run test:state # tests/integration/state/**
npm run test:deployment-bucket # tests/integration/deployment-bucket/**
npm run test:license-key # tests/integration/license-key/**
npm run test:mcp # tests/integration/mcp/(--maxWorkers=2)
其中 test:mcp 的完整定义值得注意:
cross-env NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" NODE_NO_WARNINGS=1 \
jest tests/integration/mcp/ --testPathIgnorePatterns=/node_modules/ --maxWorkers=2
限制 --maxWorkers=2 是刻意的——该套件并行部署多套真实 API Gateway + Lambda,worker 过多会撞 AWS API 限流。
三、测试环境准备:必需的环境变量
本地运行集成测试前,先导出两个变量:
export SERVERLESS_LICENSE_KEY_DEV="your-license-key"
export SERVERLESS_ACCESS_KEY_DEV="your-access-key"
SERVERLESS_LICENSE_KEY_DEV 用于框架的 license 相关校验,SERVERLESS_ACCESS_KEY_DEV 是 Serverless Dashboard 的访问密钥。CI 中对应的注入方式见 .github/workflows/ci-mcp.yml 中 Test: MCP Servers 步骤的 env 块——两个 secret 之外还额外注入:
TEST_STAGE: pr-${{ github.event.pull_request.user.login }}:为每个 PR 生成带作者前缀的 stage,避免不同 PR 之间部署资源互相冲突;SLS_AWS_SDK: 3与AWS_MAX_ATTEMPTS: 7:显式指定 AWS SDK v3 并调高重试次数,对抗部署后短暂的 API 抖动。
测试运行器对"资源刚部署完尚未生效"的窗口也有专门处理:tests/utils/testUtils.js 中的 fetchWithRetry 对任何非 2xx 响应最多重试 5 次、间隔 5 秒——注释里写明"刚部署的 API Gateway 在路由传播期间可能短暂返回 403/404",且每次重试都会先消费响应体以释放 undici 连接池。
四、AWS 前置资源清单
这是 TESTING.md 中最详尽的部分,所有资源按区域分列,本地账号需逐一对照准备。
4.1 SSM 参数(Parameter Store)
us-east-1:
| 参数路径 | 类型 | 值 |
|---|---|---|
/resolvers/sample-param |
String | ssm-value |
/resolvers/sample-secure-param |
SecureString | ssm-value |
/resolvers/sample-list-param |
StringList | foo,bar |
/resolvers/sample-json-param |
SecureString | { "foo": "bar" } |
/resolvers/object-secure-param |
SecureString | { "objectKey": "objectValue" } |
/serverless-framework/license-key-serverlesstestaccount |
SecureString | your-license-key |
/resolvers/terraform-hcp-token |
String | your-terraform-hcp-token |
eu-west-1:
| 参数路径 | 类型 | 值 |
|---|---|---|
/resolvers/sample-param |
String | ssm-value |
/resolvers/sample-secure-param-eu-west-1 |
SecureString | ssm-value |
这批参数覆盖 resolvers 集成测试(tests/integration/resolvers/)中 AWS 参数解析的各种类型分支:普通 String、SecureString、StringList、JSON 形态的安全参数,以及跨区域(us-east-1 与 eu-west-1 双区域)解析场景。
4.2 Secrets Manager 密钥
us-east-1 中需存在名为 resolvers/sample-secret 的密钥,内容为:
{
"num": 1,
"str": "secret",
"arr": [true, false]
}
4.3 S3 存储桶
| 桶名 | 要求 |
|---|---|
serverless-compose-state-bucket-integration-test |
开启版本控制(versioning),用于 compose 状态存储测试 |
terraform-s3-resolver-test-bucket |
开启版本控制,用于 Terraform S3 状态解析器测试 |
resolvers-integration-test |
内含文件 test.txt,内容为 file content |
4.4 DynamoDB 表
us-east-1:
- 表名
terraform-s3-resolver-test-lock-table,主键LockID(String 类型)——Terraform S3 后端状态锁表,对应 resolvers 对锁机制的测试。
4.5 CloudFormation 栈
两个区域各部署一个名为 sfc-nodejs-resolvers-integration-test 的栈(同账号、不同 region):
| 区域 | Output:ServerlessDeploymentBucketName |
Output:Function1LambdaFunctionQualifiedArn |
|---|---|---|
| us-east-1 | sfc-nodejs-resolvers-inte-serverlessdeploymentbuck-6vskiu5gzt1u |
arn:aws:lambda:us-east-1:762003938904:function:sfc-nodejs-resolvers-integration-test-function1:1 |
| eu-west-1 | sfc-nodejs-resolvers-inte-serverlessdeploymentbuck-vky0nzemsvvr |
arn:aws:lambda:eu-west-1:762003938904:function:sfc-nodejs-resolvers-integration-test-function1:1 |
从源码结构看,这类固定 ARN/桶名的前置栈是 tests/integration/resolvers/ 下 fixture 断言的具体对象——测试会校验 cloudformation、s3 等 resolver 能解析到这些已知资源,因此桶名与函数别名(:1 版本)必须与文档完全一致。
4.6 Cognito 前置池(MCP 强制鉴权套件专用)
tests/integration/mcp/mcp-auth.test.js 覆盖 mcp 配置项在鉴权上的两个职责——同时明确它"不做鉴权"本身:执行权在用户侧。套件为每种"控制 MCP 路由访问"的方式各部署一个 server,并断言**网关(API Gateway)**对每一种的响应行为:
- Cognito 用户池授权器(
authorizer: { arn, scopes }):使用从下述前置池真实铸造的 access token 驱动——这正是前置池存在的理由; - TOKEN 型 Lambda 授权器与 REQUEST 型 Lambda 授权器:两者都校验每次运行生成的共享密钥,并锁定拒绝时 API Gateway 返回的精确
401+{"message":"Unauthorized"}响应体; - 未配置任何
authorizer的 server:同时重新验证普通流式传输、以及 JSON-RPC notification(无 body)收到的202响应; oauthDiscovery文档(由 API Gateway MOCK 路由提供):断言其精确 body、CORS 头,以及最关键的一条属性——它恰好能被 server 路由拒绝的那个未认证客户端读取。
被拒绝的请求会从 CloudWatch 日志(而非状态码)证明其根本没有到达函数,这使"框架自身不做任何校验"成为一个被观测到的事实而非声明。
前置池的部署方式
该池是一个持久、一次性、按账号的前置条件,由 template.yml 定义,框架直接部署它——一个只含 template.yml 的目录会让 serverless deploy 路由到 CloudFormation runner,该 runner 自行传递所需的 IAM capabilities:
cd packages/sf-core/tests/integration/mcp-cognito-prerequisite
serverless deploy --stack mcp-integration-test-cognito --region us-east-1
模板实际创建的资源(对照 template.yml):
- Lite 层用户池(
UserPoolTier: LITE,名称mcp-integration-test); - 用户池域名
mcp-integration-test-${AWS::AccountId}——账号作用域保证全局唯一,并承载/oauth2/token端点; - 资源服务器
mcp,含自定义 scopeinvoke(完整 scope 串mcp/invoke)——fixture 的 authorizer 配置了这个 scope,正是这一点让 API Gateway 校验该池的原始 access token 而非 identity token; - 两个
client_credentialsM2M 应用客户端(均生成 secret、均允许mcp/invokescope):- Client A——套件用它铸造"可用"的 token;
- Client B——同池同 scope、不同 client id。套件断言它的 token 同样被接受,从而钉死一个事实:API Gateway 的 Cognito authorizer 作用域是"池 + scope",而不是某个客户端。
模板里还有一个值得学习的工程细节:CloudFormation 原生的 AWS::SSM::Parameter 资源无法创建 SecureString 参数,所以模板内联了一个 python3.12 Lambda 自定义资源(Custom::SsmSecureParams),在 /mcp-integration-test/cognito/ 前缀下写入八个 SecureString 参数(poolId、domain、region、clientAId、clientASecret、clientBId、clientBSecret、scope),并在栈删除时清理它们。配套 IAM 角色被严格限定在 parameter${SsmPrefix}/* 与自己的日志组上——没有任何账号级权限。客户端 secret 只存在于 SSM,绝不写入栈 Outputs。
套件在运行时动态发现这些参数(前缀常量见 tests/integration/mcp/lib/cognito.mjs 中的 DEFAULT_PREFIX = '/mcp-integration-test/cognito'),从 poolId 与调用者自己的账号推导池 ARN,再用 mcp/invoke scope 部署 fixture 中被 Cognito 保护的 server——没有任何硬编码 id。
跳过与失败的边界
- 前置条件确实不存在(前缀下无参数、参数不全、或根本没有凭证)→ 套件打印清晰信息并跳过,绝不硬失败一个不具备该条件的账号;
- 其他任何读取失败(被拒、限流、凭证过期、网络问题)→ 失败而非跳过:那些是"本应成功的读取",静默跳过会把"什么都没跑"伪装成"覆盖到了"。
成本方面,据 mcp-cognito-prerequisite/README.md:Cognito 的 M2M 客户端费用已取消(2025 年 11 月起),剩余仅 $0.00225/千次 token 请求,即使每月 1000 次 CI 运行也约 $0.014/月,空闲池成本为零。
MCP 套件的另一半(mcp.test.js)无需任何前置条件。两个文件都从 npm run test:mcp 触发,各自使用独立的 fixture 目录(fixture/ 与 fixture-auth/)以保证并行安全。
五、Serverless Dashboard 前置条件
Dashboard(serverless.com 账号体系)侧需要两个服务:
服务 resolvers-custom-test:
- Dashboard Parameters:
dashboard-param=dashboard-value
服务 resolver-output-producer:
- Dashboard Outputs:
outputs:
str: string-value
num: 42
obj:
foo: bar
前者供 self / dashboard 参数 resolver 测试读取,后者供跨服务 output 引用测试(tests/integration/resolvers/ 中的 output 相关 fixture)断言字符串、数值、对象三种类型的 output 解析结果。
六、Terraform Cloud 前置条件
- 组织:
serverlesstestaccount - 工作区:
serverless-test-01
配合 SSM 中的 /resolvers/terraform-hcp-token 参数,支撑 tests/integration/resolvers/terraform/ 下对 Terraform Cloud 输出解析的集成断言。
七、CI 测试账号:OIDC 替代长期密钥
TESTING.md 的 "CI Test Accounts" 一节给出了与大多数开源项目不同的凭据设计,配合 .github/workflows/ci-mcp.yml 可以完整看到落地方式:
- CI 从不使用长期 AWS 密钥:每个 workflow 通过 GitHub 的 OIDC provider 在测试账号中 assume 一个名为
GithubActionsDeploymentRole的角色(workflow 中即role-to-assume参数,如ci-mcp.yml的Setup: AWS Credentials步骤)。 - 多账号分摊套件:大多数集成套件跑在一个持有全部前置资源的账号里,但额外账号用于"分散套件"——独立 runner 打破单 runner 上限,AWS API 限流按账号隔离而不被同一次运行中的所有套件共享。MCP 套件是第一个迁移者(
ci-mcp.yml矩阵中account: test-2、角色 ARN 读自仓库变量TEST2_ROLE_ARN),因为它要部署整套 REST API,最容易撞限流。 - 新账号引导是两步人工操作,且均为按账号维度:
- OIDC provider 与部署角色由维护者内部配置;账号就绪后,其角色 ARN 以仓库变量形式提供给 workflow(见
ci-mcp.yml矩阵注释),而不是提交到仓库; - 本文档列举的前置资源(SSM 参数、secret、桶、表、栈、Cognito 池)在每个账号各自存在。某套件在其运行账号中缺少前置资源时,要么失败、要么(MCP 强制套件那样)跳过——在"从未运行"的覆盖上报绿才是真正要防的。
- OIDC provider 与部署角色由维护者内部配置;账号就绪后,其角色 ARN 以仓库变量形式提供给 workflow(见
CI: MCP Servers 展示了这个模式的形状:单腿矩阵 + 仓库变量命名账号。套件自包含且在任意引导完成的账号中行为一致,因此多账号运行是"隔离"而非"分片"——下一个迁移的套件只需加一条 matrix 腿加一个变量。
另外注意 ci-mcp.yml 的触发策略:GitHub Actions 没有 job 级路径过滤,所以过滤条件直接写在工作流自身的 pull_request/push triggers 上,只在该套件可能受影响的路径变更时运行(packages/serverless/lib/plugins/aws/mcp/**、API Gateway 事件编译代码、esbuild 插件等)。其中 lib/classes/plugin-manager.js 虽然不是 MCP 命名,却因它是 mcp 插件唯一的注册点而被列入——否则过滤条件对它失明,插件注册被删也能"绿着上线"。
八、其他测试套件
TESTING.md 最后列出的补充套件:
| 命令 | 说明 |
|---|---|
npm test -w @serverless/engine |
packages/engine 的单元测试 |
npm test -w @serverless/mcp |
MCP server 测试(不被任何 CI 工作流运行) |
npm run test:python -w @serverlessinc/sf-core |
Python 插件测试(由 CI: Python Requirements 工作流覆盖) |
npm run test:build -w @serverlessinc/sf-core |
打包/分发冒烟测试 |
补充一个文档未单列但与二进制发布直接相关的入口:packages/sf-core/package.json 中的 test:binary 脚本以 BINARY_INTEGRATION_TESTS=true 运行同一批集成测试。从 tests/utils/runSfCore.js 可以看到其机制:runSfCore() 检测该环境变量,若置位则走 runSfCoreBinary()——把命令与 options 拼成 --key=value 参数后 spawn dist/binaries/sf-core-<macos|linux|win>-<arch> 二进制子进程,以退出码和 stdout/stderr 中的 error/✖ 判定成败;否则进程内直接调用 ServerlessCore.run() 并 spy console.log 捕获错误。也就是说同一套测试代码同时验证"源码路径"与"打包后的二进制"两条链路。
九、排障指引
遇到失败时,TESTING.md 给出的统一入口是查看 packages/sf-core/tests/integration/ 目录下的测试实现与配置。结合本仓库的结构,排障时可以对照:
- 各套件目录下的
fixture/(如 mcp 套件的 fixture 与fixture-auth/)——测试实际部署的服务配置就在其中; - tests/utils/runSfCore.js 与 testUtils.js——理解错误是从哪一层(框架日志、HTTP 响应、子进程退出码)冒出来的;
- 前置条件缺失时的行为差异(硬失败 vs 带日志跳过)——先确认自己是"缺资源"还是"资源错",MCP 强制套件的跳过逻辑是判断标准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00