test-environments

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
<objective> Staging on SQLite passes tests that break on prod Postgres; a shared staging box becomes a queue where one broken deploy blocks the whole team; an unmocked Stripe call flakes CI at random. This skill prevents those by designing environment tiers that mirror production where it matters, isolate per-PR, and stub external dependencies at the HTTP boundary. It delivers a working `docker compose up` local/CI stack, a parity checklist, and a stubbing strategy keyed to dependency type. </objective>
<objective> 如果Staging环境使用SQLite,而生产环境使用PostgreSQL,那么Staging环境中通过的测试可能在生产环境中失败;共享的Staging服务器会成为瓶颈,一次失败的部署会阻塞整个团队;未桩化的Stripe调用会导致CI随机失败。本技能通过设计与生产环境关键特性一致的环境分层、按PR隔离环境、在HTTP边界层桩化外部依赖来避免这些问题。它提供可运行的`docker compose up`本地/CI栈、一致性检查表,以及按依赖类型划分的桩化策略。 </objective>

Discovery Questions

探索问题

Check
.agents/qa-project-context.md
first — if it exists, use it and skip anything already answered there. Then:
  1. How many environments exist today? Local dev, CI, staging, preview, production? Map what you have before designing what you need.
  2. Is the app containerized? Check for
    Dockerfile
    ,
    docker-compose.yml
    , or
    compose.yaml
    . If yes, multi-stage targets and compose come for free; if not, that is the first deliverable.
  3. How is test data seeded? Manual SQL, migration-based, factory libraries, or production snapshots? This decides whether seed scripts are a quick win or a rewrite.
  4. How close is staging to production? Same DB engine, queue, cache, auth provider, orchestration? Each mismatch is a class of bugs staging can never catch.
  5. External dependencies: How many third-party APIs does the system call, and are they stubbed in non-prod? Unstubbed third parties are the top source of CI flake.

首先查看
.agents/qa-project-context.md
——如果该文件存在,请使用其中的信息,并跳过已回答的问题。然后:
  1. 当前存在多少种环境? 本地开发、CI、Staging、预览、生产环境?在设计所需环境之前,先梳理现有环境的情况。
  2. 应用是否已容器化? 检查是否存在
    Dockerfile
    docker-compose.yml
    compose.yaml
    。如果已容器化,则可直接使用多阶段构建目标和Compose;如果没有,这将是首要交付成果。
  3. 测试数据如何初始化? 手动SQL脚本、基于迁移的方式、工厂库还是生产环境快照?这将决定种子脚本是快速优化还是需要重写。
  4. Staging环境与生产环境的接近程度如何? 是否使用相同的数据库引擎、队列、缓存、认证提供商、编排工具?每一处不匹配都会导致Staging环境无法发现某类Bug。
  5. 外部依赖: 系统调用多少个第三方API?在非生产环境中这些API是否已被桩化?未桩化的第三方API是CI不稳定的主要原因。

Core Principles

核心原则

1. Staging must mirror production where bugs hide. If staging uses SQLite and production uses PostgreSQL, staging tests prove nothing about prod behavior. Match the database engine and version, the queue system, the cache layer, and the auth provider — those are where environment-specific bugs live.
2. Ephemeral environments beat long-lived ones. A shared staging environment becomes a bottleneck where one broken deploy blocks the entire team. Per-PR preview environments give isolation and parallel testing; keep staging only for final pre-release validation.
3. Deterministic seed data, not production copies. Production snapshots carry PII, stale references, and non-reproducible state. Build seed data from factories that generate consistent, valid, minimal datasets. (For factory patterns, see
test-data-management
.)
4. Stub external dependencies at the boundary, not deep inside. Third-party APIs are unreliable, rate-limited, and expensive. Stub them at the HTTP boundary with MSW or WireMock — never by mocking internal service classes, which hides integration bugs between your own code.
5. Environment config is code. Every environment difference (URLs, flags, credentials, resource limits) must be version-controlled and reviewable. No manual setup that cannot be reproduced from the repo.

1. Staging环境必须在Bug高发区域与生产环境保持一致。 如果Staging使用SQLite而生产环境使用PostgreSQL,那么Staging的测试结果无法反映生产环境的行为。匹配数据库引擎及版本、队列系统、缓存层和认证提供商——这些都是环境特定Bug的高发区。
2. 临时环境优于长期运行环境。 共享的Staging环境会成为瓶颈,一次失败的部署会阻塞整个团队。按PR划分的预览环境提供隔离性和并行测试能力;仅保留Staging环境用于最终的预发布验证。
3. 使用确定性种子数据,而非生产环境副本。 生产环境快照包含PII(个人可识别信息)、陈旧引用和不可复现的状态。基于工厂模式构建种子数据,生成一致、有效、最小化的数据集。(关于工厂模式,请参考
test-data-management
。)
4. 在边界层桩化外部依赖,而非在内部深层桩化。 第三方API不可靠、有调用限制且成本高昂。使用MSW或WireMock在HTTP边界层桩化它们——绝不要通过Mock内部服务类的方式,这会隐藏自有代码之间的集成Bug。
5. 环境配置即代码。 所有环境差异(URL、功能开关、凭证、资源限制)都必须纳入版本控制并可被评审。不允许存在无法从代码仓库复现的手动配置。

Environment Strategy

环境策略

Environment Tiers

环境分层

EnvironmentPurposeDataExternal DepsLifecycle
Local devFast inner loopSeeded fixtures, minimalStubbed (MSW/WireMock)Developer-managed
CIAutomated validationSeeded per-run, ephemeralStubbed or containerizedCreated/destroyed per pipeline
PreviewPR-level review & E2ESeeded from factoriesStubbed or sandboxCreated on PR, destroyed on close
StagingPre-production validationAnonymized production-likeReal integrations (sandbox accounts)Long-lived, regularly reset
ProductionLive usersRealRealPermanent
环境用途数据外部依赖生命周期
本地开发快速内循环验证初始化的测试数据,最小化规模桩化(MSW/WireMock)开发者自主管理
CI自动化验证每次运行初始化,临时数据桩化或容器化随流水线创建/销毁
预览PR级评审与端到端测试基于工厂模式生成的初始化数据桩化或沙箱环境PR创建时生成,PR关闭时销毁
Staging预生产验证匿名化的生产级数据真实集成(沙箱账号)长期运行,定期重置
生产面向真实用户真实数据真实依赖永久运行

Local Development

本地开发

Fast feedback, zero shared state. Developers must be able to run the full stack locally in under two minutes:
bash
docker compose -f docker-compose.test.yml up -d
npm run db:seed
npm run dev
Use Docker Compose for infrastructure deps (database, cache, queue) but run the application natively for fast reload. External APIs are stubbed with MSW handlers loaded in dev mode.
快速反馈,无共享状态。开发者必须能够在两分钟内启动完整的本地栈:
bash
docker compose -f docker-compose.test.yml up -d
npm run db:seed
npm run dev
使用Docker Compose管理基础设施依赖(数据库、缓存、队列),但本地运行应用以实现快速重载。外部API通过开发模式下加载的MSW处理器进行桩化。

CI Environment

CI环境

Fully containerized, created fresh per pipeline run, destroyed after. The block below is the
services:
fragment
of a job — nest it under
jobs.<id>.services
alongside
runs-on
and
steps
; on its own it is not a valid workflow file.
yaml
undefined
完全容器化,每次流水线运行时全新创建,运行结束后销毁。以下是作业的**
services:
片段**——将其嵌套在
jobs.<id>.services
下,与
runs-on
steps
同级;单独使用时并非有效的工作流文件。
yaml
undefined

.github/workflows/test.yml — fragment: nest under jobs.test.services

.github/workflows/test.yml — 片段:嵌套在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
undefined
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
undefined

Docker Compose vs Testcontainers

Docker Compose vs Testcontainers

Two ways to give tests real infrastructure. Pick by where the lifecycle should live:
  • Docker Compose — declarative stack you bring up before the suite (
    docker compose up --wait
    ) and tear down after, usually via a
    trap
    -guarded script. Best for local dev, a shared CI stack, and E2E where many tests share one set of services.
  • Testcontainers (Node / JVM / Python / Go) — containers spun up from test code and auto-torn-down per suite or per test, with no compose file or
    trap
    to maintain. Best for integration tests that need isolated, programmatic infra (a throwaway Postgres per test class). The 2026 default for "ephemeral infra owned by the test," and a strong alternative to hand-rolled compose + trap scripts.
Reach for Compose when humans and many tests share the stack; reach for Testcontainers when each test (or suite) wants its own disposable copy.
为测试提供真实基础设施的两种方式。根据生命周期管理需求选择:
  • Docker Compose — 声明式栈,在测试套件运行前启动(
    docker compose up --wait
    ),运行后销毁,通常通过
    traps
    守护脚本实现。最适合本地开发、共享CI栈,以及多个测试共享同一服务集的端到端测试。
  • Testcontainers(Node / JVM / Python / Go) — 从测试代码中启动容器,测试套件或单个测试结束后自动销毁,无需维护compose文件或
    traps
    脚本。最适合需要隔离、可编程基础设施的集成测试(例如每个测试类使用独立的Postgres实例)。这是2026年「测试专属临时基础设施」的默认方案,也是手动编写compose+traps脚本的优质替代方案。
当人员和多个测试共享基础设施时选择Compose;当每个测试(或套件)需要独立的一次性副本时选择Testcontainers。

Preview Environments (Per-PR)

预览环境(按PR划分)

Each pull request gets its own isolated environment; reviewers click a link and test the exact changes without interfering with other PRs.
Hosting options (2026), pick by stack:
  • Vercel preview deployments — Next.js / static / serverless; per-PR URL automatically.
  • Cloudflare Pages preview — git-integrated, generous free tier.
  • Render / Railway preview environments — full-stack including databases.
  • Northflank, Qovery, Bunnyshell, Uffizzi — full ephemeral-environment platforms (Kubernetes-backed) when previews need the whole stack, not just a frontend.
For each preview, pair the env lifecycle with a database branch (Neon, Supabase, PlanetScale-style): create a branch on PR open, drop it on close. That gives every preview a cheap, instant, isolated DB copy instead of a shared staging DB. (See
test-data-management
.)
For local-dev parity with CI:
  • Devcontainers (
    .devcontainer/devcontainer.json
    ) — VS Code, Codespaces, JetBrains. The standard for "everyone gets the same Docker-backed dev env."
  • Tilt (
    Tiltfile
    ) — Kubernetes-first local dev with hot reload and multi-service orchestration. Pick when staging itself is K8s.
A frontend preview with E2E against the generated URL is a few lines:
yaml
- name: Run E2E against preview
  env:
    BASE_URL: ${{ steps.deploy.outputs.preview-url }}
  run: npx playwright test --project=chromium
A custom Docker preview keyed to a per-PR namespace, auto-torn-down on close:
yaml
- 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
每个Pull Request都拥有独立的隔离环境;评审人员点击链接即可测试确切的变更,不会干扰其他PR。
2026年的托管选项,根据技术栈选择:
  • Vercel预览部署 — 适用于Next.js / 静态站点 / Serverless;自动生成按PR划分的URL。
  • Cloudflare Pages预览 — 与Git集成,免费额度充足。
  • Render / Railway预览环境 — 包含数据库的全栈方案。
  • NorthflankQoveryBunnyshellUffizzi — 全栈临时环境平台(基于Kubernetes),适用于需要完整栈而非仅前端的预览场景。
为每个预览环境搭配数据库分支(Neon、Supabase、PlanetScale风格):PR打开时创建分支,PR关闭时删除分支。这样每个预览环境都拥有廉价、即时、隔离的数据库副本,而非共享Staging数据库。(参考
test-data-management
。)
为实现本地开发与CI环境的一致性:
  • Devcontainers
    .devcontainer/devcontainer.json
    ) — 适用于VS Code、Codespaces、JetBrains。这是「所有人使用相同Docker驱动开发环境」的标准方案。
  • Tilt
    Tiltfile
    ) — 面向Kubernetes的本地开发方案,支持热重载和多服务编排。当Staging环境本身基于K8s时选择此方案。
针对生成的URL运行端到端测试的前端预览配置只需几行代码:
yaml
- name: Run E2E against preview
  env:
    BASE_URL: ${{ steps.deploy.outputs.preview-url }}
  run: npx playwright test --project=chromium
按PR命名空间划分、PR关闭时自动销毁的自定义Docker预览配置:
yaml
- 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

Staging

Staging

Long-lived environment that mirrors production infrastructure. Reset weekly or on-demand to prevent drift:
bash
#!/bin/bash
与生产基础设施一致的长期运行环境。每周或按需重置以避免环境漂移:
bash
#!/bin/bash

scripts/reset-staging.sh

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."

**Caveat:** `DROP SCHEMA public CASCADE` also drops the schema's default privileges and any
installed extensions (`uuid-ossp`, `pgcrypto`, …). Your migration pipeline must recreate them
(`CREATE EXTENSION IF NOT EXISTS …`, re-grant defaults) or the migrate step fails. Don't assume
a bare `CREATE SCHEMA public` restores the prior grants — it does not.

---
set -euo pipefail
echo "Resetting staging database..." psql "$STAGING_DATABASE_URL" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
echo "Running migrations..." # 迁移必须重新创建扩展和权限(见下方注意事项) 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 for Testing

Docker Compose测试方案

A production-quality
docker-compose.test.yml
spins up the full stack (app, Postgres, Redis, a one-shot seed container, Mailpit) for integration and E2E tests. Two details that matter:
  • Health checks gate
    depends_on
    .
    Without a
    healthcheck
    +
    condition: service_healthy
    ,
    depends_on
    only waits for the container to start, not for the service to accept connections — tests then race the database and fail with connection errors.
  • Seed is a one-shot container, not a long-running service. It uses
    depends_on: condition: service_completed_successfully
    , so the app starts only after seeding exits 0. Teams that model seed as a long-running service get a race where the app boots mid-seed.
See
references/docker-compose.md
for the full
docker-compose.test.yml
, the
trap
-guarded integration test runner, the multi-stage Dockerfile (with the
production
target), and the MinIO block.
生产级别的
docker-compose.test.yml
会启动完整栈(应用、Postgres、Redis、一次性初始化容器、Mailpit)以支持集成测试和端到端测试。以下两个细节至关重要:
  • 健康检查控制
    depends_on
    如果没有
    healthcheck
    +
    condition: service_healthy
    depends_on
    仅等待容器启动,而非服务就绪并可接受连接——此时测试会与数据库竞争资源,因连接错误而失败。
  • 初始化是一次性容器,而非长期运行服务。 使用
    depends_on: condition: service_completed_successfully
    ,确保应用仅在初始化容器*成功退出(exit 0)*后启动。将初始化建模为长期运行服务的团队会遇到竞争问题:应用在初始化过程中启动。
完整的
docker-compose.test.yml
traps
守护的集成测试运行器、多阶段Dockerfile(含
production
目标)及MinIO配置,请参考
references/docker-compose.md

Multi-Stage Dockerfile

多阶段Dockerfile

One
base
layer installs deps once;
development
,
test
, and
seed
stages reuse it; and a slim
production
stage runs prod deps only (
npm ci --omit=dev
) with build artifacts copied from the
test
stage. The split keeps test dependencies and source out of the shipped image while giving each environment its own entrypoint. Use
npm ci --include=dev
in
base
— the modern flag;
--production=false
is legacy
--omit
/
--include
syntax. Full Dockerfile in
references/docker-compose.md
.

一个
base
层一次性安装依赖;
development
test
seed
阶段复用该层;精简的
production
阶段仅运行生产依赖(
npm ci --omit=dev
),并从
test
阶段复制构建产物。这种拆分可将测试依赖和源码排除在发布镜像之外,同时为每个环境提供独立的入口点。在
base
层使用
npm ci --include=dev
——这是现代语法;
--production=false
是旧版
--omit
/
--include
语法。完整Dockerfile请参考
references/docker-compose.md

External Dependency Management

外部依赖管理

Stubbing Strategy by Dependency Type

按依赖类型划分的桩化策略

Dependency TypeLocal/CI StrategyStaging Strategy
Payment (Stripe)MSW handler returning mock responsesStripe test mode with
sk_test_
keys
Email (SendGrid)Mailpit capturing SMTP (web UI on :8025, SMTP on :1025)SendGrid sandbox mode
Auth (Auth0)Local JWT issuer with test keysAuth0 dev tenant
Storage (S3)MinIO container (S3-compatible)Dedicated test bucket with lifecycle policy
Search (Elasticsearch)Testcontainers ElasticsearchDedicated test index with reset script
SMS (Twilio)MSW handlerTwilio test credentials
Avoid: MailHog — unmaintained, last release 2020. Use Mailpit (
axllent/mailpit
); it is a drop-in on the same ports (1025 SMTP / 8025 UI).
依赖类型本地/CI策略Staging策略
支付(Stripe)MSW处理器返回模拟响应使用
sk_test_
密钥的Stripe测试模式
邮件(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
);它是MailHog的无缝替代,使用相同端口(1025 SMTP / 8025 UI)。

MSW for HTTP Stubs

MSW用于HTTP桩化

Stub external APIs at the HTTP boundary with MSW 2.x:
http
+
HttpResponse
from
msw
,
setupServer
from
msw/node
, lifecycle wired through
beforeAll
/
afterEach
/
afterAll
. Set
onUnhandledRequest: "error"
so an unmocked external call fails the test loudly instead of leaking a real network request. See
references/stubbing.md
for the Stripe/SendGrid/geocoding handlers and the server lifecycle.
使用MSW 2.x在HTTP边界层桩化外部API:从
msw
导入
http
+
HttpResponse
,从
msw/node
导入
setupServer
,通过
beforeAll
/
afterEach
/
afterAll
管理生命周期。设置
onUnhandledRequest: "error"
,这样未桩化的外部调用会直接导致测试失败,而非静默发起真实网络请求。Stripe/SendGrid/地理编码处理器及服务器生命周期配置,请参考
references/stubbing.md

MinIO as an S3 Substitute

MinIO作为S3替代方案

Run S3-compatible storage in a container instead of hitting real AWS in local/CI tests. Point the AWS SDK
S3Client
at it with
endpoint
, env-var credentials, and
forcePathStyle: true
(required for MinIO). Compose service + client config in
references/docker-compose.md
.
在容器中运行兼容S3的存储服务,避免在本地/CI测试中调用真实AWS服务。通过
endpoint
、环境变量凭证和
forcePathStyle: true
(MinIO必需)配置AWS SDK的
S3Client
。Compose服务及客户端配置请参考
references/docker-compose.md

Contract Testing as Stub Validation

契约测试作为桩化验证手段

Stubs drift from reality. Pair every stub with a contract test that verifies the stub matches the real API shape. For details, see
contract-testing
.

桩化实现会与真实API产生偏差。为每个桩化实现搭配契约测试,验证桩化响应与真实API结构一致。详情请参考
contract-testing

Environment Parity Checklist

环境一致性检查表

Run this when setting up or auditing a non-production environment.
DimensionQuestionRed Flag
Database engineSame engine and version as production?SQLite in test, PostgreSQL in prod
Database schemaSame migration pipeline applied?Manual schema changes in staging
Data shapeSeed data covers all entity states?Only "happy path" records, no edge cases
InfrastructureSame container orchestration?Docker Compose in CI, Kubernetes in prod
NetworkSame internal service topology?Monolith in test, microservices in prod
ConfigEnv vars documented and version-controlled?Undocumented env vars, manual setup
AuthSame auth provider/flow?Bypassed auth in test with hardcoded tokens
Feature flagsSame flag evaluation engine?Hardcoded flags in test, LaunchDarkly in prod
TLS/HTTPSSame certificate handling?HTTP in staging, HTTPS in prod
Timeouts/LimitsSame rate limits, pools, timeouts?Infinite timeouts in test hide perf issues
For factory-based seed data patterns, see
test-data-management
.

在搭建或审计非生产环境时使用此检查表。
维度问题风险信号
数据库引擎是否与生产环境使用相同引擎及版本?测试环境用SQLite,生产环境用PostgreSQL
数据库 schema是否应用了相同的迁移流水线?Staging环境存在手动schema变更
数据结构种子数据是否覆盖所有实体状态?仅包含「正常路径」记录,无边缘情况
基础设施是否使用相同的容器编排工具?CI环境用Docker Compose,生产环境用Kubernetes
网络是否拥有相同的内部服务拓扑?测试环境是单体应用,生产环境是微服务
配置环境变量是否已文档化并纳入版本控制?存在未文档化的环境变量、手动配置
认证是否使用相同的认证提供商/流程?测试环境通过硬编码令牌绕过认证
功能开关是否使用相同的开关评估引擎?测试环境用硬编码开关,生产环境用LaunchDarkly
TLS/HTTPS是否使用相同的证书处理方式?Staging环境用HTTP,生产环境用HTTPS
超时/限制是否使用相同的速率限制、连接池、超时设置?测试环境无超时限制,隐藏性能问题
基于工厂模式的种子数据模式,请参考
test-data-management

Anti-Patterns

反模式

Shared staging as the only test environment. One developer's broken deploy blocks everyone. Use ephemeral per-PR environments for isolation and keep staging for final pre-release validation only.
Production database copies for test data. PII risk, non-reproducible state, massive datasets that slow tests. Build minimal seed data from factories with deterministic values.
Environment-specific code paths.
if (process.env.NODE_ENV === "test") { skipAuth(); }
means you are not testing the real auth flow. Swap implementations via dependency injection or config, not environment conditionals.
Manual environment setup. If setup needs a 15-step wiki page, it will be wrong within a week. Script everything:
docker compose up -d && npm run db:seed
should be the only steps.
Stubbing internal services instead of external ones. Stub at the HTTP boundary where your system talks to the outside world. Stubbing internal modules hides integration bugs between your own services.
No health checks in Docker Compose.
depends_on
without a healthcheck waits only for the container to start, not for the service to be ready — tests race the database and fail with connection errors.
Long-lived preview environments. Previews that persist after merge waste resources and accumulate stale state. Automate teardown on PR close (
if: github.event.action == 'closed'
).

仅使用共享Staging作为测试环境。 开发者的一次失败部署会阻塞所有人。使用按PR划分的临时环境实现隔离,仅保留Staging用于最终预发布验证。
使用生产数据库副本作为测试数据。 存在PII泄露风险、不可复现状态,且数据集过大导致测试缓慢。基于工厂模式构建最小化的确定性种子数据。
环境特定代码路径。
if (process.env.NODE_ENV === "test") { skipAuth(); }
意味着未测试真实认证流程。通过依赖注入或配置切换实现,而非环境条件判断。
手动环境配置。 如果配置需要15步的wiki文档,一周内就会出现错误。将所有操作脚本化:
docker compose up -d && npm run db:seed
应是唯一需要执行的步骤。
桩化内部服务而非外部依赖。 在系统与外部交互的HTTP边界层进行桩化。桩化内部模块会隐藏自有服务之间的集成Bug。
Docker Compose中未配置健康检查。 无健康检查的
depends_on
仅等待容器启动,而非服务就绪——测试会与数据库竞争资源,因连接错误而失败。
长期运行的预览环境。 合并后仍保留的预览环境会浪费资源并积累陈旧状态。在PR关闭时自动销毁(
if: github.event.action == 'closed'
)。

Verification

验证步骤

Run these against the artifacts you produce, smallest check first:
  1. Compose file is valid
    docker compose -f docker-compose.test.yml config -q
    exits 0 (catches YAML and schema errors before you ever pull an image).
  2. Stack comes up healthy
    docker compose -f docker-compose.test.yml up -d --wait --wait-timeout 60
    exits 0; a non-zero exit means a healthcheck never went green.
  3. Database accepts connections
    docker compose exec postgres pg_isready -U test -d testdb
    reports
    accepting connections
    .
  4. Dockerfile builds the production target
    docker build --target production -t app:prod .
    succeeds, and
    docker run --rm app:prod npm ls --omit=dev --depth=0
    shows no dev deps.
  5. Stubs fail loud — run the suite with
    onUnhandledRequest: "error"
    ; any real outbound call should error the test, not pass silently.

针对交付成果执行以下验证,从最小检查项开始:
  1. Compose文件有效
    docker compose -f docker-compose.test.yml config -q
    返回0(在拉取镜像前捕获YAML和schema错误)。
  2. 栈健康启动
    docker compose -f docker-compose.test.yml up -d --wait --wait-timeout 60
    返回0;非0返回值表示健康检查从未通过。
  3. 数据库可接受连接
    docker compose exec postgres pg_isready -U test -d testdb
    返回
    accepting connections
  4. Dockerfile可构建生产目标
    docker build --target production -t app:prod .
    执行成功,且
    docker run --rm app:prod npm ls --omit=dev --depth=0
    显示无开发依赖。
  5. 桩化失败时触发错误 — 使用
    onUnhandledRequest: "error"
    运行测试套件;任何真实外部调用都应导致测试失败,而非静默通过。

Done When

交付标准

  • Environment inventory documented (dev, CI, preview, staging, production) with characteristics and access notes per tier.
  • docker compose -f docker-compose.test.yml config -q
    exits 0 and
    docker compose up -d --wait
    brings every service to a passing healthcheck (exit 0).
  • Multi-stage Dockerfile builds the
    production
    target with
    --omit=dev
    (no dev dependencies in the shipped image).
  • Seed scripts are idempotent (running twice exits 0, no duplicate-key errors) and checked into the repository.
  • External dependencies are stubbed at the HTTP boundary with
    onUnhandledRequest: "error"
    ; no real third-party credentials in non-prod.
  • Environment parity gaps documented (e.g. SQLite in CI vs PostgreSQL in prod) with mitigations in place or tracked as issues.
  • Preview environments auto-created for PRs and auto-torn-down on close (
    if: github.event.action == 'closed'
    ).

  • 已记录环境清单(开发、CI、预览、Staging、生产),包含各层级的特性及访问说明。
  • docker compose -f docker-compose.test.yml config -q
    返回0,且
    docker compose up -d --wait
    使所有服务通过健康检查(返回0)。
  • 多阶段Dockerfile可构建
    production
    目标,且使用
    --omit=dev
    (发布镜像中无开发依赖)。
  • 种子脚本具有幂等性(运行两次返回0,无重复键错误),并已提交至代码仓库。
  • 外部依赖已在HTTP边界层桩化,且设置
    onUnhandledRequest: "error"
    ;非生产环境中无真实第三方凭证。
  • 已记录环境一致性差距(例如CI用SQLite vs 生产用PostgreSQL),并已采取缓解措施或跟踪为问题。
  • PR打开时自动创建预览环境,PR关闭时自动销毁(
    if: github.event.action == 'closed'
    )。

Reference Files (in
references/
)

参考文件(位于
references/

  • docker-compose.md — full
    docker-compose.test.yml
    (Postgres 18, Redis 8, one-shot seed, Mailpit), the
    trap
    -guarded integration test runner, the multi-stage Dockerfile (base/development/test/seed/production), and the MinIO service + S3 client config.
  • stubbing.md — MSW 2.x handlers for Stripe/SendGrid/geocoding and the
    setupServer
    lifecycle with
    onUnhandledRequest: "error"
    .

  • docker-compose.md — 完整的
    docker-compose.test.yml
    (Postgres 18、Redis 8、一次性初始化容器、Mailpit)、
    traps
    守护的集成测试运行器、多阶段Dockerfile(base/development/test/seed/production)、MinIO服务及S3客户端配置。
  • stubbing.md — Stripe/SendGrid/地理编码的MSW 2.x处理器,以及设置
    onUnhandledRequest: "error"
    setupServer
    生命周期配置。

Related Skills

相关技能

  • service-virtualization — Decision framework for choosing mock vs stub vs fake vs real per dependency, and WireMock/MSW depth. Go there to decide the stubbing approach; this skill wires the chosen stub into the environment.
  • test-data-management — Factory patterns, synthetic data, database seeding, and DB branching (Neon/Supabase/PlanetScale) for per-PR DB copies.
  • ci-cd-integration — Pipeline config, GitHub Actions services, artifact management, sharding, and self-hosted runners. Go there for the surrounding workflow; this skill defines the services it runs against.
  • contract-testing — Consumer-driven contracts that verify your stubs match real APIs.
  • service-virtualization — 针对单个依赖选择Mock/Stub/Fake/真实实现的决策框架,以及WireMock/MSW的深度配置。如需确定桩化方案,请参考该技能;本技能负责将选定的桩化方案接入环境。
  • test-data-management — 工厂模式、合成数据、数据库初始化、数据库分支(Neon/Supabase/PlanetScale)用于按PR划分数据库副本。
  • ci-cd-integration — 流水线配置、GitHub Actions服务、制品管理、分片、自托管运行器。如需配置周边工作流,请参考该技能;本技能定义工作流运行的服务环境。
  • contract-testing — 消费者驱动契约,验证桩化实现与真实API一致。