3. Testing框架核心机制:测试用例编写规范、断言库使用、Mock与Stub技术、Fixture与参数化

在容器化故障排查场景中,Testing 框架是保障压测脚本可靠性与可复现性的基石。本章深入剖析框架的四大核心机制,帮助你在混沌工程与故障注入场景中编写健壮的测试用例。

3.1 测试用例编写规范

容器化环境下的测试用例需遵循“原子性、可重复、可隔离”原则。每个用例应只验证一个故障场景或一个恢复动作。

规范维度 要求 容器化场景示例
命名规则 test_<故障类型>_<预期行为> test_network_delay_connection_timeout
用例结构 Arrange → Act → Assert 注入延迟 → 发起请求 → 断言超时
隔离性 不依赖外部状态,每次运行前重置容器 使用 docker restart 或 kubectl delete pod
幂等性 多次执行结果一致 故障注入量固定,如 tc qdisc add dev eth0 root netem delay 100ms
避坑提示: 不要在测试用例中硬编码容器 IP 或端口。应通过环境变量或 Fixture 动态获取,否则在容器重启或扩缩容后用例会直接失败。

3.2 断言库使用

断言是验证故障注入效果的“裁判”。在容器化压测中,断言不仅要检查状态码,还需验证延迟、资源使用、日志输出等指标。

3.2.1 常用断言模式

# 基础断言
assert response.status_code == 200
assert response.elapsed.total_seconds() < 5.0

# 容器状态断言
assert container.status == "running"
assert float(container.attrs['State']['StartedAt']) > threshold

# 日志内容断言
assert "connection refused" in container.logs().decode()

# 指标范围断言
assert 0.5 <= cpu_percent <= 0.8, f"CPU 使用率异常: {cpu_percent}"

3.2.2 断言库对比

库 适用场景 容器化优势
unittest 标准测试框架 内置,无需额外依赖
pytest 高级断言与插件 原生支持 Fixture 和参数化
assertpy 链式断言 可读性强,适合复杂指标验证
推荐: 在容器化故障排查场景中,优先使用 pytest + assertpy 组合。pytest 提供强大的 Fixture 和参数化支持,assertpy 让断言链更清晰。

3.3 Mock与Stub技术

在容器化环境中,Mock 和 Stub 用于模拟外部依赖(如数据库、消息队列、第三方 API),避免故障注入影响真实服务。

3.3.1 Mock vs Stub 对比

特性 Mock Stub
行为验证 验证是否被调用、调用次数、参数 只返回预设数据,不验证调用
适用场景 验证故障恢复逻辑是否触发 模拟外部服务返回固定响应
容器化示例 Mock 健康检查接口,验证重试机制 Stub 数据库查询,返回预设错误码

3.3.2 容器化Mock实践

from unittest.mock import patch

def test_container_health_check_retry():
    with patch('requests.get') as mock_get:
        # 模拟前两次失败,第三次成功
        mock_get.side_effect = [
            ConnectionError("timeout"),
            ConnectionError("timeout"),
            Mock(status_code=200)
        ]
        result = health_check_with_retry("http://container:8080/health")
        assert result is True
        assert mock_get.call_count == 3
注意: 在容器化环境中,Mock 应只用于单元测试或集成测试的边界场景。对于端到端故障注入,应使用真实的容器网络故障(如 tc 命令),而非 Mock。

3.4 Fixture与参数化

Fixture 是容器化测试的“基础设施”,负责容器的启动、配置、故障注入和清理。参数化则让一个测试用例覆盖多种故障参数。

3.4.1 Fixture 生命周期

import pytest
import docker

@pytest.fixture(scope="module")
def container():
    """启动一个 Nginx 容器作为测试目标"""
    client = docker.from_env()
    container = client.containers.run(
        "nginx:alpine",
        detach=True,
        ports={'80/tcp': None}
    )
    yield container
    # 清理:停止并删除容器
    container.remove(force=True)

def test_nginx_responds(container):
    response = requests.get(f"http://{container.attrs['NetworkSettings']['IPAddress']}")
    assert response.status_code == 200

3.4.2 参数化故障注入

@pytest.mark.parametrize("delay_ms,expected_timeout", [
    (100, False),   # 100ms 延迟,不应超时
    (500, False),   # 500ms 延迟,不应超时
    (2000, True),   # 2000ms 延迟,应超时
])
def test_network_delay(container, delay_ms, expected_timeout):
    # 注入网络延迟
    container.exec_run(f"tc qdisc add dev eth0 root netem delay {delay_ms}ms")
    try:
        response = requests.get(
            f"http://{container.attrs['NetworkSettings']['IPAddress']}",
            timeout=1
        )
        assert not expected_timeout
    except requests.Timeout:
        assert expected_timeout
    finally:
        # 清理网络规则
        container.exec_run("tc qdisc del dev eth0 root")
最佳实践: 将 Fixture 的 scope 设置为 module 或 session,避免每个用例都重新启动容器。参数化时,将故障参数(延迟、丢包率、带宽限制)作为变量,实现“一个用例,多种故障”。

3.5 核心机制关系图

下图展示了 Testing 框架四大核心机制在容器化故障排查中的协作关系:

Testing框架核心机制协作图 Testing框架核心机制协作图 测试用例 编写规范 原子性·可重复·可隔离 断言库 状态码·延迟·日志 Mock / Stub 模拟外部依赖 Fixture / 参数化 容器生命周期·多参数 协作流程 1. Fixture 启动容器并注入故障参数 2. 测试用例按规范编排步骤,使用 Mock/Stub 隔离外部依赖 3. 断言库验证故障注入后的容器状态与响应 容器化故障排查压测场景

3.6 综合实践:故障注入测试模板

将上述机制整合为一个完整的容器化故障注入测试模板:

import pytest
import docker
import requests
from unittest.mock import patch

# Fixture:启动目标容器
@pytest.fixture(scope="module")
def target_container():
    client = docker.from_env()
    container = client.containers.run(
        "nginx:alpine",
        detach=True,
        ports={'80/tcp': None}
    )
    yield container
    container.remove(force=True)

# 参数化:多种故障类型
@pytest.mark.parametrize("fault_type,params", [
    ("delay", {"delay_ms": 200}),
    ("packet_loss", {"loss_percent": 10}),
    ("bandwidth", {"rate": "1mbit"}),
])
def test_network_fault_injection(target_container, fault_type, params):
    """验证不同网络故障对容器响应的影响"""
    # Arrange:注入故障
    if fault_type == "delay":
        cmd = f"tc qdisc add dev eth0 root netem delay {params['delay_ms']}ms"
    elif fault_type == "packet_loss":
        cmd = f"tc qdisc add dev eth0 root netem loss {params['loss_percent']}%"
    elif fault_type == "bandwidth":
        cmd = f"tc qdisc add dev eth0 root tbf rate {params['rate']} burst 32kbit latency 400ms"
    
    target_container.exec_run(cmd)
    
    # Act:发起请求
    ip = target_container.attrs['NetworkSettings']['IPAddress']
    try:
        response = requests.get(f"http://{ip}", timeout=5)
        # Assert:故障下仍应返回 200
        assert response.status_code == 200
    except requests.Timeout:
        # 超时也是预期行为之一
        assert fault_type == "delay" and params["delay_ms"] > 1000
    finally:
        # 清理:移除故障
        target_container.exec_run("tc qdisc del dev eth0 root")
扩展阅读: 在实际压测中,可将此模板与 Prometheus 指标采集结合,在断言中增加对 CPU、内存、网络重传率的验证,实现更全面的故障影响评估。