返回测试开发模块
Test Development / Tutorial 12

Docker 与 Kubernetes 测试环境实战教程

把测试环境当作工程资产来管理:一键启动、随时排障、用完即毁。围绕「一键启动订单系统测试环境」学会容器化的完整套路。

8 个章节订单系统测试环境案例Docker + Kubernetes
01

为什么测试需要容器

可重复 · 可排障 · 可销毁
容器化测试环境的完整生命周期
构建镜像锁定运行环境
一键拉起Compose 编排
初始化数据SQL 脚本
执行测试pytest 用例
销毁清理down 清卷

测试环境的三个工程目标

  • 可重复:同一份镜像与脚本,在任何机器上得到相同结果。
  • 可排障:日志、健康状态、端口映射都可随时查看。
  • 可销毁:环境是临时的,删除后不留下任何残留。

传统环境 vs 容器环境

维度传统方式容器方式
环境一致性换一台机器就出现行为差异构建产物固定镜像,人人拿到相同环境
启动速度手工安装数据库、配置依赖耗时以小时计一条 compose 命令秒级拉起整套服务
服务隔离测试互相污染,数据残留独立容器、独立网络与端口,互不干扰
与开发对齐测试依赖版本与开发环境漂移镜像同时承载运行时与依赖版本
清理成本卸载残留、进程残留难查down 命令一键销毁容器、网络与卷
测试环境不是“搭一次用很久”的基础设施,而是每次执行都能重建的临时资源。判断标准很简单:删掉它,还能不能再花一分钟把它原样拉起来。
02

镜像与容器基础

一次构建,处处运行

三个核心概念

  • 镜像:只读的模板,包含操作系统、运行时、依赖和应用代码。
  • 容器:镜像的运行实例,拥有独立的文件系统与网络。
  • 层:镜像由一层层增量文件组成,复用层让拉取与构建更快。
order-service/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
常用命令速查
docker build -t order-service:test .
docker run -d --name order-mysql -e MYSQL_ROOT_PASSWORD=test_root -p 13306:3306 mysql:8.0
docker ps -a
docker logs --tail=50 order-mysql
docker exec -it order-mysql bash
给镜像固定 tag(如 order-service:test),不要依赖 latest。latest 每次构建结果都可能不同,会直接破坏“可重复”这个目标。
03

用 docker-compose 编排测试环境

一条命令拉起全家桶
docker-compose.yml
services:
  mysql:
    image: mysql:8.0
    container_name: order-mysql
    environment:
      MYSQL_ROOT_PASSWORD: test_root
      MYSQL_DATABASE: order_test
      MYSQL_USER: order
      MYSQL_PASSWORD: order_test
    ports:
      - "13306:3306"
    volumes:
      - ./init:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ptest_root"]
      interval: 5s
      timeout: 3s
      retries: 20

  redis:
    image: redis:7-alpine
    container_name: order-redis
    ports:
      - "16379:6379"

  order-service:
    build: ./order-service
    container_name: order-service
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      DB_HOST: mysql
      DB_PORT: "3306"
      DB_USER: order
      DB_PASSWORD: order_test
      REDIS_HOST: redis
    ports:
      - "18000:8000"

  order-tests:
    build: ./order-tests
    container_name: order-tests
    depends_on:
      order-service:
        condition: service_started
    environment:
      ORDER_API: http://order-service:8000
    volumes:
      - ./reports:/app/reports

这张编排表做了什么

  • MySQL 与 Redis 使用官方镜像,负责存储与缓存。
  • order-service 是被测服务,通过服务名访问数据库和缓存。
  • order-tests 挂载宿主机 reports 目录,让报告能落盘。
  • 健康检查保证 MySQL 就绪后被测服务才启动。
端口映射把容器端口暴露到宿主机:13306 是宿主机端口,3306 是容器内端口。测试代码在容器内一律使用服务名,只有宿主机工具才用映射端口。
04

测试数据初始化与清理

删掉才能重建
init/init.sql
CREATE TABLE IF NOT EXISTS orders (
  order_id VARCHAR(64) PRIMARY KEY,
  status VARCHAR(32) NOT NULL,
  amount DECIMAL(10, 2) NOT NULL,
  created_at DATETIME NOT NULL
);

INSERT IGNORE INTO orders (order_id, status, amount, created_at)
VALUES ('SEED-0001', 'PENDING_PAYMENT', 99.90, NOW());

初始化脚本的约定

  • MySQL 镜像会在首次建库后自动执行 /docker-entrypoint-initdb.d 下的脚本。
  • 把 init 目录挂载到该路径,脚本随 compose 一起版本化管理。
  • 脚本必须幂等:重复执行不会报错、不会产生脏数据。
  • 只准备最小数据集,具体测试数据由用例自己创建。
测试后的清理策略
# 删除容器、网络与数据卷,环境完全归零
docker compose down -v

# 只想清数据、保留服务时,逐表清空
docker compose exec mysql mysql -uorder -porder_test order_test -e "SET FOREIGN_KEY_CHECKS=0; TRUNCATE TABLE orders;"

# 重启一套全新环境
docker compose up -d
可重复的关键不在于“手动恢复现场”,而在于“销毁后原样重建”。down -v 删掉的卷会在下次 up 时由初始化脚本重新生成,这才是真正的干净状态。
05

在容器里执行测试并收集结果

退出码即结果
在容器内运行 pytest
# 用一次性容器执行测试,跑完自动删除
docker compose run --rm order-tests pytest -q

# 挂载 reports 目录,把 JUnit 与 HTML 报告输出到宿主机
docker compose run --rm -v ./reports:/app/reports order-tests pytest --junitxml=/app/reports/junit.xml --html=/app/reports/report.html
整套环境跑完自动退出
docker compose up --build --abort-on-container-exit --exit-code-from order-tests
echo "exit code: $?"

如何判定结果

  • pytest 通过时退出码为 0,失败时非 0。
  • --exit-code-from 会把 order-tests 的退出码作为整条命令的退出码。
  • CI 里直接以这条命令的退出码决定构建通过或失败。
  • JUnit 报告让测试结果能进 CI 平台,HTML 报告方便人工查看。
让测试在容器里跑而不是在宿主机跑,意味着任何人只要执行同一条命令,就能拿到完全相同的用例集、依赖版本和初始数据。结果可信的前提是环境可重复。
06

Kubernetes 临时测试环境

命名空间即隔离边界

为什么测试环境要用 Kubernetes

  • 命名空间把环境隔离成独立区域,删除命名空间即销毁全部资源。
  • Pod 承载服务,Deployment 管理副本与重启,ConfigMap/Secret 存放配置。
  • 贴近生产调度方式,能发现只在多实例下出现的并发问题。
  • 临时环境用完即毁,不占用常驻资源。
order-test.yaml(命名空间 + 数据库)
apiVersion: v1
kind: Namespace
metadata:
  name: order-test
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-mysql
  namespace: order-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: order-mysql
  template:
    metadata:
      labels:
        app: order-mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.0
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: test_root
          ports:
            - containerPort: 3306
kubectl 常用命令
kubectl create namespace order-test
kubectl -n order-test apply -f order-test.yaml
kubectl -n order-test get pods -w
kubectl -n order-test logs deploy/order-mysql --tail=50
kubectl -n order-test describe pod order-mysql
kubectl -n order-test delete deployment order-mysql
kubectl delete namespace order-test
敏感配置放进 Secret 而不是明文写在 YAML 或环境变量里。临时环境创建快、销毁也快,但排障手段和正式环境完全一致,这正是“可排障”的来源。
07

常见排障手册

先看状态再翻日志

症状与对策速查

症状典型报错对策
端口冲突报错 address already in usedocker ps 找占用容器,或换用 13306 之类映射端口
容器间网络不通连接被拒绝容器内必须用服务名 mysql/redis,不要用 localhost
数据库没就绪就执行测试pytest 连不上数据库配置 healthcheck 并用 depends_on 等待健康
时间差 8 小时时间戳与预期不符给容器加 TZ=Asia/Shanghai 环境变量
中文乱码写入的中文变成问号数据库建表指定 utf8mb4,容器设置 LANG 环境
排障命令模板
docker ps -a
docker compose ps
docker compose logs --tail=50 mysql
docker port order-mysql
docker exec -it order-mysql mysqladmin ping -h localhost
docker exec -it order-service sh -c "curl http://mysql:3306"
docker stats

排障的顺序

  • 先看健康状态:compose ps、kubectl get pods 确认服务是否 Running。
  • 再看日志:logs 是服务自己留下的证据,比猜测更可靠。
  • 再验证连通:从被测服务容器内部去访问依赖,而不是从宿主机访问。
  • 最后检查资源:docker stats 确认 CPU 与内存没有被限制拖垮。
容器环境最大的排障陷阱是用宿主机视角看问题:端口映射只对宿主机生效,容器之间永远走服务名。遇到连接失败,先进容器里用 curl 试一次。
08

练习与完成清单

动手把环境跑起来

练习:一键启动订单系统测试环境

  1. 用 docker build 构建 order-service 镜像,并用 docker images 确认 tag。
  2. 编写 init/init.sql,挂载后首次启动自动建表并插入 SEED-0001 订单。
  3. 用 docker compose config 校验编排文件语法是否正确。
  4. 故意把宿主端口改成 13306,确认宿主机从 13306 可连接、容器内用 3306。
  5. 在容器内运行 pytest 并故意制造一条失败用例,确认退出码非 0。
  6. 执行 docker compose down -v 后再次 up,确认数据回到初始化状态。
  7. 用 kubectl 创建命名空间 order-test 部署 MySQL,验证 Pod 状态 Running。
  8. 执行 kubectl delete namespace order-test,确认资源全部消失。

可重复

  • 镜像版本固定
  • 初始化脚本幂等
  • 启动顺序有依赖
  • 一条命令拉起全套

可排障

  • 日志可随时查看
  • 健康检查有状态
  • 退出码可判定结果
  • 端口映射清楚明确

可销毁

  • down -v 清理卷
  • 命名空间级隔离
  • 数据可原样重建
  • 宿主机零残留

测试环境不是搭一次用很久的设施,而是可重复、可排障、可销毁的工程资产。

返回测试开发模块