首页
/ Serverless Framework 测试体系解析:从单元测试到 AWS 集成测试环境搭建(TESTING.md 深度指南)

Serverless Framework 测试体系解析:从单元测试到 AWS 集成测试环境搭建(TESTING.md 深度指南)

2026-09-05 18:45:47作者:江焘钦

本文基于仓库根目录的 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.ymlTest: MCP Servers 步骤的 env 块——两个 secret 之外还额外注入:

  • TEST_STAGE: pr-${{ github.event.pull_request.user.login }}:为每个 PR 生成带作者前缀的 stage,避免不同 PR 之间部署资源互相冲突;
  • SLS_AWS_SDK: 3AWS_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 断言的具体对象——测试会校验 cloudformations3 等 resolver 能解析到这些已知资源,因此桶名与函数别名(:1 版本)必须与文档完全一致。

4.6 Cognito 前置池(MCP 强制鉴权套件专用)

tests/integration/mcp/mcp-auth.test.js 覆盖 mcp 配置项在鉴权上的两个职责——同时明确它"不做鉴权"本身:执行权在用户侧。套件为每种"控制 MCP 路由访问"的方式各部署一个 server,并断言**网关(API Gateway)**对每一种的响应行为:

  1. Cognito 用户池授权器authorizer: { arn, scopes }):使用从下述前置池真实铸造的 access token 驱动——这正是前置池存在的理由;
  2. TOKEN 型 Lambda 授权器REQUEST 型 Lambda 授权器:两者都校验每次运行生成的共享密钥,并锁定拒绝时 API Gateway 返回的精确 401 + {"message":"Unauthorized"} 响应体;
  3. 未配置任何 authorizer 的 server:同时重新验证普通流式传输、以及 JSON-RPC notification(无 body)收到的 202 响应;
  4. 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,含自定义 scope invoke(完整 scope 串 mcp/invoke)——fixture 的 authorizer 配置了这个 scope,正是这一点让 API Gateway 校验该池的原始 access token 而非 identity token;
  • 两个 client_credentials M2M 应用客户端(均生成 secret、均允许 mcp/invoke scope):
    • 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 参数(poolIddomainregionclientAIdclientASecretclientBIdclientBSecretscope),并在栈删除时清理它们。配套 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 可以完整看到落地方式:

  1. CI 从不使用长期 AWS 密钥:每个 workflow 通过 GitHub 的 OIDC provider 在测试账号中 assume 一个名为 GithubActionsDeploymentRole 的角色(workflow 中即 role-to-assume 参数,如 ci-mcp.ymlSetup: AWS Credentials 步骤)。
  2. 多账号分摊套件:大多数集成套件跑在一个持有全部前置资源的账号里,但额外账号用于"分散套件"——独立 runner 打破单 runner 上限,AWS API 限流按账号隔离而不被同一次运行中的所有套件共享。MCP 套件是第一个迁移者(ci-mcp.yml 矩阵中 account: test-2、角色 ARN 读自仓库变量 TEST2_ROLE_ARN),因为它要部署整套 REST API,最容易撞限流。
  3. 新账号引导是两步人工操作,且均为按账号维度
    • OIDC provider 与部署角色由维护者内部配置;账号就绪后,其角色 ARN 以仓库变量形式提供给 workflow(见 ci-mcp.yml 矩阵注释),而不是提交到仓库;
    • 本文档列举的前置资源(SSM 参数、secret、桶、表、栈、Cognito 池)在每个账号各自存在。某套件在其运行账号中缺少前置资源时,要么失败、要么(MCP 强制套件那样)跳过——在"从未运行"的覆盖上报绿才是真正要防的。

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 套件的 fixturefixture-auth/)——测试实际部署的服务配置就在其中;
  • tests/utils/runSfCore.jstestUtils.js——理解错误是从哪一层(框架日志、HTTP 响应、子进程退出码)冒出来的;
  • 前置条件缺失时的行为差异(硬失败 vs 带日志跳过)——先确认自己是"缺资源"还是"资源错",MCP 强制套件的跳过逻辑是判断标准。
登录后查看全文
热门项目推荐
相关项目推荐