关于 test-environments
test-environments 是面向开发与 QA 团队的测试环境设计技能,帮助规划与生产关键部分一致、可隔离且可复现的环境。内容涵盖环境分层、容器化配置、依赖替身和环境一致性检查。
主要功能
- 01规划本地、CI、预览与暂存环境
- 02通过 Docker Compose 配置测试栈
- 03按依赖类型制定外部服务替身策略
- 04使用 MSW 在 HTTP 边界模拟 API
- 05提供环境一致性检查清单
适用场景
- 01减少暂存环境与生产环境差异导致的漏测
- 02为每个 PR 隔离预览环境
- 03避免 CI 测试调用真实第三方 API
- 04搭建可复现的本地与 CI 测试栈
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ petrkindlmann/ qa-skills --skill test-environments安装后在 Claude Code 中输入/test-environments确认已加载。
附带文件references/docker-compose.mdreferences/stubbing.md
详细介绍
调研问题
先检查 .agents/qa-project-context.md——如果文件存在,就使用其中的信息,并跳过已经回答的问题。然后:
- 目前有多少个环境? 本地开发、CI、预发布、预览、生产?在设计所需环境前,先梳理现有环境。
- 应用是否容器化? 检查是否存在
Dockerfile、docker-compose.yml或compose.yaml。如果是,多阶段构建目标和 Compose 配置都可直接使用;如果不是,这就是首要交付项。 - 如何填充测试数据? 手动 SQL、基于迁移、工厂库,还是生产快照?这决定了编写种子脚本是快速改进还是需要重写。
- 预发布环境与生产环境有多接近? 数据库引擎、队列、缓存、身份验证提供方、编排方式是否相同?每一处不一致都意味着预发布环境永远无法捕获一类错误。
- 外部依赖: 系统会调用多少个第三方 API?它们在非生产环境中是否经过模拟?未经模拟的第三方依赖是 CI 不稳定的首要来源。
核心原则
1. 预发布环境必须在容易出现错误的地方与生产环境保持一致。 如果预发布环境使用 SQLite,而生产环境使用 PostgreSQL,那么预发布测试无法证明生产环境中的行为正确。数据库引擎及其版本、队列系统、缓存层和身份验证提供方都应保持一致——环境相关的错误往往出现在这些地方。
2. 临时环境优于长期运行的环境。 共享的预发布环境会成为瓶颈:一次有问题的部署就会阻塞整个团队。每个 PR 配备独立的预览环境,可以隔离环境并支持并行测试;预发布环境仅用于发布前的最终验证。
3. 使用确定性的种子数据,而非生产数据副本。 生产快照包含个人身份信息、过时引用和无法复现的状态。使用工厂构建种子数据,生成一致、有效且精简的数据集。(关于工厂模式,请参阅 test-data-management。)
4. 在边界处模拟外部依赖,而不是在深层代码中模拟。 第三方 API 不可靠、存在速率限制且成本高昂。使用 MSW 或 WireMock 在 HTTP 边界处模拟它们——不要通过模拟内部服务类来实现,因为这会掩盖自有代码间的集成错误。
5. 将环境配置视为代码。 每项环境差异(URL、标志、凭据、资源限制)都必须纳入版本控制并可供审查。不得进行无法从代码仓库复现的手动设置。
环境策略
环境层级
| 环境 | 用途 | 数据 | 外部依赖 | 生命周期 |
|---|---|---|---|---|
| 本地开发 | 快速迭代 | 填充种子数据的固定数据,精简 | 已模拟(MSW/WireMock) | 由开发者管理 |
| CI | 自动化验证 | 每次运行时填充种子数据,临时 | 已模拟或容器化 | 每条流水线创建并销毁 |
| 预览 | PR 级审查和端到端测试 | 由工厂填充种子数据 | 已模拟或沙盒环境 | 创建 PR 时创建,关闭时销毁 |
| 预发布 | 生产前验证 | 匿名化且接近生产环境的数据 | 真实集成(沙盒账户) | 长期运行,定期重置 |
| 生产 | 服务真实用户 | 真实数据 | 真实依赖 | 永久运行 |
本地开发
快速反馈,零共享状态。开发者必须能够在两分钟内于本地运行完整技术栈:
docker compose -f docker-compose.test.yml up -d
npm run db:seed
npm run dev
使用 Docker Compose 管理基础设施依赖(数据库、缓存、队列),但在本机原生运行应用以实现快速重载。外部 API 使用在开发模式下加载的 MSW 处理器进行模拟。
CI 环境
完全容器化,每次流水线运行时全新创建,结束后销毁。下面的代码块是作业的
services: 片段——将其嵌套在 jobs.<id>.services 下,与 runs-on
和 steps 同级;单独使用并不是有效的工作流文件。
# .github/workflows/test.yml — fragment: nest under jobs.test.services
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports: ['5432:5432']
options: >-
--health-cmd="pg_isready -U test"
--health-interval=5s
--health-timeout=3s
--health-retries=5
redis:
image: redis:8-alpine
ports: ['6379:6379']
options: >-
--health-cmd="redis-cli ping"
--health-interval=5s
--health-timeout=3s
--health-retries=5
Docker Compose 与 Testcontainers
为测试提供真实基础设施的两种方式。根据生命周期应该由谁管理来选择:
- Docker Compose — 以声明式方式定义服务栈,在测试套件运行前启动(
docker compose up --wait),结束后关闭,通常通过由trap保护的脚本完成。最适合本地开发、共享的 CI 服务栈,以及许多测试共用一组服务的 E2E 测试。 - Testcontainers(Node / JVM / Python / Go)——从测试代码中启动容器,并在每个测试套件或测试结束后自动清理,无需维护 compose 文件或
trap。最适合需要隔离、可通过代码控制的基础设施的集成测试(例如,每个测试类使用一个临时 Postgres)。这是 2026 年“由测试管理的临时基础设施”的默认选择,也是手动编写 compose + trap 脚本的有力替代方案。
当人们和许多测试共用服务栈时,选择 Compose;当每个测试(或套件)都需要独立的临时副本时,选择 Testcontainers。
预览环境(每个 PR 一个)
每个拉取请求都有自己的隔离环境;审查者可以点击链接,测试确切的改动,且不会影响其他 PR。
托管选项(2026 年),根据技术栈选择:
- Vercel 预览部署——适用于 Next.js / 静态网站 / 无服务器应用;自动为每个 PR 生成 URL。
- Cloudflare Pages 预览——与 Git 集成,并提供慷慨的免费套餐。
- Render / Railway 预览环境——支持包含数据库在内的全栈应用。
- Northflank、Qovery、Bunnyshell、Uffizzi——完整的临时环境平台(基于 Kubernetes),适用于预览需要整个服务栈而不只是前端的情况。
为每个预览环境配套一个数据库分支(Neon、Supabase、PlanetScale 风格):PR 开启时创建分支,关闭时删除。这样每个预览环境都有一个低成本、即时创建的隔离数据库副本,而不是共用暂存数据库。(参见 test-data-management。)
为了让本地开发环境与 CI 保持一致:
- Devcontainers(
.devcontainer/devcontainer.json)——适用于 VS Code、Codespaces、JetBrains。是“每个人都使用相同的 Docker 支持开发环境”的标准方案。 - Tilt(
Tiltfile)——以 Kubernetes 为先的本地开发方案,支持热重载和多服务编排。适用于暂存环境本身也运行在 K8s 上的情况。
为前端创建预览环境,并针对生成的 URL 运行 E2E 测试,只需几行代码:
- name: Run E2E against preview
env:
BASE_URL: ${{ steps.deploy.outputs.preview-url }}
run: npx playwright test --project=chromium
按每个 PR 的命名空间创建自定义 Docker 预览环境,并在 PR 关闭时自动清理:
- name: Deploy preview
run: |
NAMESPACE="pr-${{ github.event.number }}"
docker compose -f docker-compose.preview.yml -p "$NAMESPACE" up -d
echo "preview-url=https://${NAMESPACE}.preview.example.com" >> "$GITHUB_OUTPUT"
- name: Teardown preview
if: github.event.action == 'closed'
run: |
NAMESPACE="pr-${{ github.event.number }}"
docker compose -p "$NAMESPACE" down -v
暂存环境
长期运行的环境,基础设施与生产环境相仿。每周或按需重置,以防止配置漂移:
#!/bin/bash
# scripts/reset-staging.sh
set -euo pipefail
echo "Resetting staging database..."
psql "$STAGING_DATABASE_URL" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
echo "Running migrations..." # migrations MUST recreate extensions + grants (see caveat below)
npm run db:migrate -- --env staging
echo "Seeding anonymized data..."
npm run db:seed -- --env staging --dataset production-anonymized
echo "Verifying staging health..."
curl -sf https://staging.example.com/health || exit 1
echo "Staging reset complete."
注意: DROP SCHEMA public CASCADE 也会删除该架构的默认权限和已安装的扩展(uuid-ossp、pgcrypto 等)。迁移流水线必须重新创建它们(CREATE EXTENSION IF NOT EXISTS …,重新授予默认权限),否则迁移步骤会失败。不要以为只执行 CREATE SCHEMA public 就能恢复之前的授权——事实并非如此。
用于测试的 Docker Compose
docker-compose.test.yml 示例会启动完整的技术栈(应用、Postgres、Redis、一次性 seed 容器、Mailpit),用于集成测试和 E2E 测试。需要注意的细节:
- 健康检查是
depends_on的前置条件。 如果没有healthcheck和condition: service_healthy,depends_on只会等待容器启动,不会等待服务接受连接——随后测试就会与数据库发生竞争,并因连接错误而失败。 - Seed 是一次性容器,不是长期运行的服务。 它使用
depends_on: condition: service_completed_successfully,因此只有在 seed 以状态码 0 退出后,应用才会启动。将 seed 建模为长期运行的服务会导致竞争问题,使应用在 seed 过程中启动。 - 在主机上运行的测试需要发布端口。 示例会将主机端口绑定到回环地址,并确保运行器 URL 与之匹配。并行运行的技术栈需要使用不同的项目名称和主机端口。
- PostgreSQL 18 使用不同的卷布局。 挂载到
/var/lib/postgresql,作为其带版本号的数据目录;其他主版本请参阅镜像文档。
有关完整的 docker-compose.test.yml、带尽力清理且保留原始退出状态的集成测试运行器、多阶段 Dockerfile(含 production 目标)以及 MinIO 部分,请参阅 references/docker-compose.md。
多阶段 Dockerfile
一个 base 层只安装一次依赖;development、test 和 seed 阶段会复用该层;精简的 production 阶段只运行生产依赖(npm ci --omit=dev),并从 test 阶段复制构建产物。这种划分能让测试依赖和源代码不进入交付的镜像,同时为每个环境提供独立的入口点。在 base 中使用 npm ci --include=dev——这是现代标志;--production=false 是旧式的 --omit/--include 语法。完整 Dockerfile 见 references/docker-compose.md。
外部依赖管理
按依赖类型划分的模拟策略
| 依赖类型 | 本地/CI 策略 | 预发布环境策略 |
|---|---|---|
| 支付(Stripe) | 使用 MSW 处理程序返回模拟响应 | 使用 Stripe 测试模式和 sk_test_ 密钥 |
| 电子邮件(SendGrid) | Mailpit 捕获 SMTP(Web UI 端口为 :8025,SMTP 端口为 :1025) | SendGrid 沙盒模式 |
| 身份验证(Auth0) | 使用测试密钥的本地 JWT 签发方 | Auth0 开发租户 |
| 存储(S3) | MinIO 容器(兼容 S3) | 配有生命周期策略的专用测试存储桶 |
| 搜索(Elasticsearch) | Testcontainers Elasticsearch | 配有重置脚本的专用测试索引 |
| 短信(Twilio) | MSW 处理程序 | Twilio 测试凭据 |
避免使用:MailHog——该项目已无人维护,最后一次发布于 2020 年。请使用 Mailpit(axllent/mailpit);它使用相同端口(1025 SMTP / 8025 UI),可直接替换。
使用 MSW 模拟 HTTP 请求
使用 MSW 2.x 在 HTTP 边界模拟外部 API:从 msw 导入 http 和 HttpResponse,从 msw/node 导入 setupServer,并通过 beforeAll/afterEach/afterAll 配置生命周期。设置 onUnhandledRequest: "error",让未模拟的外部调用明确导致测试失败,而不是泄漏真实网络请求。Stripe/SendGrid/地理编码处理程序和服务器生命周期详见 references/stubbing.md。
使用 MinIO 替代 S3
在本地/CI 测试中,通过容器运行兼容 S3 的存储,而不是访问真实的 AWS。将 AWS SDK 的 S3Client 指向它,并设置 endpoint、环境变量凭据和 forcePathStyle: true(MinIO 必需)。Compose 服务和客户端配置见 references/docker-compose.md。
使用契约测试验证模拟
模拟可能会偏离实际情况。为每个模拟配套编写契约测试,验证其是否符合真实 API 的结构。详情请参阅 contract-testing。
环境一致性检查清单
设置或审查非生产环境时,请运行此检查。
| 维度 | 问题 | 风险信号 |
|---|---|---|
| 数据库引擎 | 是否与生产环境使用相同的引擎和版本? | 测试环境使用 SQLite,生产环境使用 PostgreSQL |
| 数据库架构 | 是否应用了相同的迁移流程? | 在预发布环境中手动更改架构 |
| 数据形态 | 种子数据是否覆盖了所有实体状态? | 只有“顺利路径”记录,没有边缘情况 |
| 基础设施 | 是否使用相同的容器编排方式? | CI 中使用 Docker Compose,生产环境使用 Kubernetes |
| 网络 | 内部服务拓扑是否相同? | 测试环境使用单体架构,生产环境使用微服务 |
| 配置 | 环境变量是否有文档记录并纳入版本控制? | 环境变量未记录,需手动设置 |
| 身份验证 | 身份验证提供方和流程是否相同? | 测试环境绕过身份验证并使用硬编码令牌 |
| 功能开关 | 是否使用相同的开关评估引擎? | 测试环境使用硬编码开关,生产环境使用 LaunchDarkly |
| TLS/HTTPS | 证书处理方式是否相同? | 预发布环境使用 HTTP,生产环境使用 HTTPS |
| 超时/限制 | 速率限制、连接池和超时设置是否相同? | 测试环境使用无限超时,掩盖性能问题 |
有关基于工厂的种子数据模式,请参阅 test-data-management。
反模式
将共享预发布环境作为唯一的测试环境。 一位开发者的部署故障会阻碍所有人。 使用每个 PR 独立的临时环境来实现隔离,并仅将预发布环境用于发布前的最终验证。
使用生产数据库副本作为测试数据。 存在个人身份信息风险,状态无法复现,数据集还会过于庞大,拖慢测试。使用工厂构建包含确定性值的最小种子数据。
特定于环境的代码路径。 if (process.env.NODE_ENV === "test") { skipAuth(); }
意味着你没有测试真实的身份验证流程。应通过依赖注入或配置切换实现,而不是使用环境条件判断。
手动设置环境。 如果设置环境需要一份 15 步的维基页面,那么一周内它就会过时。将所有步骤脚本化:docker compose up -d && npm run db:seed 应是唯一需要执行的步骤。
用桩替代内部服务,而非外部服务。 在系统与外部世界交互的 HTTP 边界处设置桩。为内部模块设置桩会掩盖自有服务之间的集成错误。
Docker Compose 中没有健康检查。 如果 depends_on 未配置健康检查,它只会等待容器启动,而不会等待服务就绪——测试会与数据库发生竞态,并因连接错误而失败。
长期运行的预览环境。 合并后仍保留的预览环境会浪费资源并积累过时状态。自动在 PR 关闭时清理环境(if: github.event.action == 'closed')。
验证
针对生成的制品运行以下检查,从最简单的检查开始:
- Compose 文件有效 —
docker compose -f docker-compose.test.yml config -q以状态码 0 退出(在拉取镜像之前就能发现 YAML 和架构错误)。 - 整个服务栈健康启动 —
docker compose -f docker-compose.test.yml up -d --wait --wait-timeout 60以状态码 0 退出。若失败,请检查输出:健康检查通过之前,启动也可能因镜像拉取、构建或种子数据命令而失败。 - 数据库接受连接 —
docker compose -f docker-compose.test.yml exec postgres pg_isready -U test -d testdb报告accepting connections。还要运行主机上的测试运行器,验证其连接 URL 使用已发布的端口。 - Dockerfile 可构建生产目标 —
docker build --target production -t app:prod .成功,并且docker run --rm app:prod npm ls --omit=dev --depth=0显示没有开发依赖。 - 桩会明确报错 — 使用
onUnhandledRequest: "error"运行测试套件;任何真实的出站调用都应使测试报错,而不是静默通过。
完成标准
- 已记录环境清单(开发、CI、预览、预发布、生产),并说明每个层级的特征和访问注意事项。
docker compose -f docker-compose.test.yml config -q和up -d --wait均以状态码 0 退出;长时间运行的服务通过其配置的就绪检查,一次性种子任务以状态码 0 退出。- 多阶段 Dockerfile 使用
--omit=dev构建production目标(发布的映像中不包含开发依赖)。 - 种子脚本具有幂等性(运行两次均以状态码 0 退出,且不会出现重复键错误),并已提交到代码仓库。
- 在 HTTP 边界对外部依赖进行存根处理,并设置
onUnhandledRequest: "error";非生产环境中不使用真实的第三方凭据。 - 已记录环境间的差异(例如,CI 使用 SQLite,而生产环境使用 PostgreSQL),并落实缓解措施或将其作为问题跟踪。
- 为 PR 自动创建预览环境,并在 PR 关闭时自动销毁(
if: github.event.action == 'closed')。
参考文件(位于 references/ 中)
- docker-compose.md — 完整的
docker-compose.test.yml(Postgres 18、Redis 8、一次性种子任务、Mailpit)、由trap保护的集成测试运行器、多阶段 Dockerfile(base/development/test/seed/production),以及 MinIO 服务和 S3 客户端配置。 - stubbing.md — 用于 Stripe/SendGrid/地理编码的 MSW 2.x 处理程序,以及带有
onUnhandledRequest: "error"的setupServer生命周期配置。
相关技能
- service-virtualization — 用于按依赖选择 mock、stub、fake 或真实实现的决策框架,以及 WireMock/MSW 的详细介绍。需要决定存根处理方式时,请参阅该技能;本技能负责将选定的存根接入环境。
- test-data-management — 工厂模式、合成数据、数据库种子数据,以及用于创建每个 PR 独立数据库副本的数据库分支(Neon/Supabase/PlanetScale)。
- ci-cd-integration — 流水线配置、GitHub Actions 服务、制品管理、分片和自托管运行器。需要了解周边工作流时,请参阅该技能;本技能定义其运行所依赖的服务。
- contract-testing — 用于验证存根与真实 API 相符的消费者驱动契约。