3.4 开发与测试环境模板


3.4 开发与测试环境模板

本节摘要:开发与测试环境模板解决"环境不一致"这个老问题:开发环境用绑定挂载加热重载、mock 外部依赖、Mailpit 本地邮箱;测试环境用测试容器加一次性数据库、健康检查前置、隔离网络。核心思想是把环境差异消灭在 Docker 层,让新同事入职当天就能跑起项目,让 CI 与本地跑同一份配置。

核心问题

  1. 能搭一个代码绑定挂载加热重载的开发环境
  2. 能设计 mock 外部依赖的容器替身,不连生产环境
  3. 能配置 Mailpit 本地邮箱并在应用里接入
  4. 能写测试容器加一次性数据库的测试环境
  5. 能理解 healthcheck 前置与 depends_on 条件等待的关系

一、开发环境:绑定挂载与热重载

开发环境的第一诉求是"改代码立刻生效"。实现手段是绑定挂载加开发模式启动命令,模板如下:

services: web: build: context: ./app dockerfile: Dockerfile.dev ports: - "5000:5000" volumes: - ./app:/app environment: - FLASK_ENV=development depends_on: - db restart: always db: image: postgres:14 ports: - "5432:5432" environment: - POSTGRES_USER=dev - POSTGRES_PASSWORD=dev - POSTGRES_DB=devdb volumes: - db_data:/var/lib/postgresql/data volumes: db_data:

配套的 Dockerfile.dev 与生产 Dockerfile 分开维护:

FROM python:3.9-slim-buster WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 5000 CMD ["flask", "run", "--host=0.0.0.0"]
  • volumes: - ./app:/app:宿主机代码目录盖住容器内 /app。注意镜像里 COPY 进的文件会被挂载遮住,启动命令用的是挂载后的版本——这正是我们要的效果。
  • FLASK_ENV=development:Flask 的开发模式开关。flask run 在该模式下自带 reloader 和 debugger,保存 .py 文件后进程自动重载,浏览器刷新即见新代码。
  • 端口冲突是开发环境的高频事故:宿主机 5000 被另一个项目占着时,容器能起但访问不到,先 netstat -ano | findstr 5000 查占用,再改左侧端口即可。
  • 依赖变更才需要重建:pip install 的依赖装在镜像层里,改 requirements.txt 后要重新 docker compose -f docker-compose.dev.yml up --build;纯代码改动不用 build。
  • Dockerfile.dev 与生产 Dockerfile 分离:开发镜像可以多装调试工具、开着 debug,生产镜像做精简。两个 Dockerfile 的差异不要靠注释约定,直接分成两个文件,构建时用 dockerfile 参数指定。

Node.js 版本的热重载:Express 进程默认不会自动重启,要加 nodemon 之类的工具,package.json 的 dev 脚本写成 nodemon index.js,Dockerfile.dev 的 CMD 指向它。热重载的机制因语言而异,但挂载的做法完全一致。

变体:VS Code Remote 开发容器:配合 devcontainer.json 可以把整个开发环境收进编辑器,配置里指定 dockerComposeFile、要进入的 service、工作目录 workspaceFolder 和要转发的端口 forwardPorts。好处是格式化工具、语言服务器、调试器全在容器里,换机器零迁移成本;代价是第一次构建镜像要等几分钟。

依赖锁文件必须提交。热重载解决了代码同步,依赖漂移是另一个污染源:两个人分别 build 镜像,pip 或 npm 解析出不同版本,行为就不一致。规矩是锁文件进仓库:Python 用 pip-tools 或 uv 生成 requirements.lock,Node.js 提交 package-lock.json,Dockerfile 里 pip install -r requirements.locknpm ci 按锁文件安装。npm cinpm install 的区别就在这里:ci 严格按锁文件装,install 会重新解析,开发容器里用错命令,锁文件等于没锁。

二、mock 外部依赖

开发环境最怕"依赖别人的服务才能启动"。第三方支付、短信网关、外部 API,这些服务在开发期要么没有测试环境,要么调用要钱。我们的做法是在 compose 里起一个替身容器,应用代码通过环境变量切换地址:

services: web: build: ./app ports: - "5000:5000" environment: - PAYMENT_API_URL=http://mock-payment:8080 - SMS_API_URL=http://mock-sms:8080 mock-payment: image: wiremock/wiremock:3.5.0 volumes: - ./mocks/payment:/home/wiremock mock-sms: image: wiremock/wiremock:3.5.0 volumes: - ./mocks/sms:/home/wiremock

WireMock 是常用的 API 模拟器,把预定义的响应 JSON 放进挂载目录,它就按路径返回固定数据。替身容器的价值在于:接口契约被写成了文件,随代码仓库走,新同事 clone 下来就能跑,不会出现"我这边连不上测试环境"的扯皮。

⚠️ mock 的边界要划清楚:只 mock 外部依赖,不要 mock 自己的数据库和消息队列。自己家的组件用真容器跑,才能在开发期暴露真实的 schema 变更和消息格式问题。

三、本地邮箱:Mailpit

应用要发邮件时,开发期绝不能真发。Mailpit 是本地 SMTP 测试工具,接收所有邮件并提供一个 Web 界面查看:

services: mailpit: image: axllent/mailpit:latest ports: - "1025:1025" - "8025:8025" web: build: ./app environment: - SMTP_HOST=mailpit - SMTP_PORT=1025 - SMTP_FROM=dev@example.com
  • 1025 是 SMTP 收信端口,应用把邮件服务器指向 mailpit 服务名即可;8025 是 Web 界面,浏览器打开就能看到收件箱、邮件正文和附件。
  • 不用配账号密码,不用真域名,发出去的每封信都进本地收件箱。调试验证码、密码重置这类流程,比真邮箱快得多。
  • Mailpit 还能模拟发信失败、延迟投递等异常场景,邮件发送模块的容错逻辑可以在开发期就测到。
  • 切换生产邮箱时只改环境变量,应用代码零改动——SMTP 配置从环境变量读,这个习惯在 3.1 节提过,在这里是硬性要求。

四、数据库测试库

开发库和测试库混用是测试结果不可靠的头号原因。模板里给数据库单独建测试库,用初始化脚本在首次启动时生成:

services: db: image: postgres:14 environment: - POSTGRES_USER=dev - POSTGRES_PASSWORD=dev - POSTGRES_DB=devdb volumes: - db_data:/var/lib/postgresql/data - ./initdb:/docker-entrypoint-initdb.d:ro

initdb 目录里放两个脚本:01_create_testdb.sql 建 testdb 库,02_seed.sql 灌基础数据。应用连接串按环境切换:开发连 devdb,测试连 testdb。测试跑完要清理数据时,直接 docker compose exec db dropdb -U dev testdb 再重建,比逐表 DELETE 干净利落。

表结构变更走迁移工具。initdb 脚本只负责"首次建库",上线后的表结构变更要靠迁移工具:Python 生态用 Alembic、Java 生态用 Flyway 或 Liquibase,Node.js 生态各有对应方案。迁移文件按版本号递增,应用启动时自动检查并执行未应用的迁移。这个习惯和容器化的配合点在于:测试环境每次重建库,迁移工具从头跑到最新版本,正好验证了迁移脚本的完整性;如果哪天重建后迁移报错,说明迁移链上有缺口,趁早修比上线后修便宜得多。

测试数据用工厂不用手写 SQL。灌测试数据最忌在用例里硬编码 INSERT,表结构一变全挂。业界通行做法是数据工厂:代码里定义对象构造器,字段默认值集中管理,用例里只覆盖关心的字段。数据库模板只管"库存在、表结构对",数据怎么造是测试代码的事,两层职责分开,测试环境才稳定。

五、测试环境:测试容器加一次性数据库

自动化测试环境的核心形态是:被测服务加依赖服务加一个跑测试的容器,测试容器跑完即退出,退出码决定成败:

services: api: build: context: ./api dockerfile: Dockerfile ports: - "3000:3000" environment: - NODE_ENV=test depends_on: - db db: image: mongo:latest volumes: - db_data:/data/db test: build: context: ./test dockerfile: Dockerfile environment: - API_URL=http://api:3000 depends_on: - api command: npm run test restart: on-failure volumes: db_data:
  • command: npm run test:测试容器的主进程就是测试命令,进程退出码就是测试结果。0 代表全部通过,非 0 代表有失败——这个约定让 compose 能直接当 CI 用。
  • restart: on-failure:测试失败时自动重启重跑。本地调试时这个策略很方便;CI 里要改成不重启,让失败现场保留在日志里。
  • 测试容器的 Dockerfile 与 API 的分开,node:16 基础镜像里 COPY 测试代码,CMD 写成 npm test
  • 数据库用真容器但不映射端口:测试只在内部网络访问,宿主机不用关心端口占用,也切断了外部干扰。

执行方式docker compose -f docker-compose.test.yml up --build,全程日志输出到终端;只跑一次并回收用 docker compose run --rm test,跑完自动删容器,不会残留。

六、依赖服务健康检查前置

测试容器最怕的不是测试失败,而是"数据库还没就绪测试就开跑",然后报一堆连接超时,看起来像代码问题。正确的做法是让 depends_on 带上条件:

services: api: build: ./api depends_on: db: condition: service_healthy db: image: postgres:14 healthcheck: test: ["CMD-SHELL", "pg_isready -U dev -d devdb"] interval: 5s timeout: 5s retries: 10 test: build: ./test depends_on: api: condition: service_healthy

condition: service_healthy 的含义:等依赖服务的健康检查通过后才启动本服务。它把"睡 10 秒再连"这种赌博式写法彻底干掉——机器慢就多等,快就少等,永远等的是就绪信号而不是拍脑袋的时长。注意 api 服务自己也要配 healthcheck,否则条件永远不满足,compose 会卡在等待里。

七、隔离网络

开发和测试环境跑在同一台机器时,网络隔离是硬要求。compose 默认每个项目一个独立网络,但同一项目里混跑开发与测试就要显式分开:

services: api: networks: - testnet db: networks: - testnet test: networks: - testnet networks: testnet: driver: bridge

三个要点:一是测试环境的服务只进 testnet,不给它访问开发网络的能力;二是网络名带项目前缀或环境后缀,project_testnetproject_devnet 一眼可辨;三是测试容器不映射任何端口,它只在内部网络里访问 API——端口映射是给宿主机用的,测试不需要。

💡 一份配置多用:把 dev 和 test 的差异收敛到环境变量里,用 -f docker-compose.yml -f docker-compose.override.yml 叠加配置,公共部分只写一份。第 4 章还会用同样的思路叠加生产配置。

八、环境文件的拆分与清理纪律

开发与测试环境共用一套模板时,最大的隐患是配置串味:测试连了开发库、开发用了测试的 mock 地址,跑出来的结果谁都不信。拆分靠的是环境文件:

# 开发环境用 docker compose --env-file .env.dev -f docker-compose.yml up -d # 测试环境用 docker compose --env-file .env.test -f docker-compose.yml up

compose 里所有 ${变量} 引用都从环境文件取值:.env.dev 里写开发库地址、mock 开关、调试级别;.env.test 里写测试库地址、测试报告路径。两个文件都进版本仓库,新同事 clone 下来就能跑。唯一的纪律是:密码和密钥不进环境文件,它们该走 CI 的密钥仓库或本机未提交的 .env.local,避免把生产凭据提交进 git 历史。

测试报告的落盘。测试容器退出后,容器内的报告文件就没了。把报告目录挂出来:

test: build: ./test volumes: - ./test-results:/app/reports

挂载后不管是 pytest 的 html 报告还是 Jest 的覆盖率目录,都会留在宿主机 test-results 里,CI 能直接收集成产物,本地也能打开看。注意挂载目录的属主:测试容器以 root 跑没问题,非 root 镜像要先把宿主机目录的写权限放开。

down 与 -v 的杀伤力docker compose down 默认不删卷,数据还在;加上 -v 会连命名卷一起删。测试环境里 down -v 是"彻底重置"的利器,一个命令把测试库、缓存全部清空回到出厂状态。但手滑在开发环境执行,开发数据库就没了。我们的规矩:测试环境允许用 -v,开发环境永远只写 down,重置开发库用初始化脚本而不是删卷。

CI 里的用法。流水线里跑测试时,环境变量的来源是流水线注入而不是 .env 文件:docker compose --env-file <(echo "$ENV_CONTENT") up --exit-code-from test。--exit-code-from 让 compose 以指定容器的退出码收尾,测试失败时流水线立即标红,不用再解析日志。这套写法在 GitHub Actions 和 Jenkins 里通用,模板本身不用改。

开发与测试环境对照

维度 开发环境 测试环境
代码来源 绑定挂载实时同步 镜像内固定版本
数据 可持久化保留 一次性或可重建
外部依赖 mock 容器替身 同样 mock 或真容器
启动方式 up 常驻 up 跑完退出
结果判定 人工观察 退出码自动化判定
端口 按需映射 一律不映射
网络 开发专用网络 隔离测试网络

要点速记

  1. 热重载靠挂载加开发模式:代码目录盖进容器,启动命令带 reloader
  2. 依赖变更才重建镜像:改 requirements 或 package.json 才需要 build
  3. 外部依赖用替身容器 mock:WireMock 类工具把契约写成文件随仓库走
  4. 邮件走 Mailpit:1025 收信、8025 看信,应用只认环境变量
  5. 测试库与开发库分离:初始化脚本建 testdb,测试数据随意清
  6. 测试容器退出码即结果:0 通过非 0 失败,compose 直接当 CI 用
  7. 健康检查前置代替 sleep:condition 用 service_healthy,等就绪信号
  8. 隔离网络加零端口映射:测试环境在内部网络闭环运行

下一节给部署栈加上眼睛——监控与日志栈模板,让每个服务的健康状态都变成看得见的数字。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U