TT Lab
开始
学习 学习路径 课程

测试工具实战

注入故障以发现资源泄漏

在 TT Lab 中继续学习

目标

观察正常关闭、重复关闭、失败路径,以及应用 lifespan 的启动与清理。

为什么重要

请求成功时有关闭连接的代码,但一旦发生异常,已打开的连接就留了下来。只确认成功响应的测试,发现不了这种泄漏。生命周期测试应当关注的是资源何时打开、何时关闭,而不是返回值。

步骤

  1. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:new_resource() 返回 {open:False, events:[]} 形式的新字典,多次调用之间不共享 events。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/test-lifespan-cleanup-lab
test -e /root/work/test-lifespan-cleanup-lab/service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/service.py /root/work/test-lifespan-cleanup-lab/service.py
test -e /root/work/test-lifespan-cleanup-lab/test_service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/test_service.py /root/work/test-lifespan-cleanup-lab/test_service.py
cd /root/work/test-lifespan-cleanup-lab
  1. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:start(resource) 在资源已经打开时抛出 ValueError,否则把 open 改为 True,并在 events 中追加 'open'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  2. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:stop(resource) 仅在资源处于打开状态时,才把 open 改为 False,并在 events 中追加 'close'。如果已经关闭,则保持原样。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  3. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:read(resource) 在资源已关闭时抛出 RuntimeError,处于打开状态时返回 {ready:True}。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  4. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:scope(resource) 是一个 contextmanager。进入时执行 start,在块内 yield 出 resource,无论块成功还是失败,都用 stop 关闭。块内的异常要继续向上传播。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  5. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:lifespan_for(resource) 返回 asynccontextmanager 函数 lifespan(app)。它在 scope(resource) 内设置 app.state.resource 并 yield。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  6. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:create_app(resource) 使用 lifespan_for。GET /ready 返回对 app.state.resource 执行 read 的结果。context 结束时必须关闭资源。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  7. 在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:exercise(resource, fail=False) 在 with TestClient(create_app(resource)) 内调用 GET /ready。如果 fail=True,就在其中抛出 RuntimeError,否则返回响应 JSON。两种情况下资源都必须被关闭。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

参考

让资源状态相互独立——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:new_resource() 返回 {open:False, events:[]} 形式的新字典,多次调用之间不共享 events。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/test-lifespan-cleanup-lab
test -e /root/work/test-lifespan-cleanup-lab/service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/service.py /root/work/test-lifespan-cleanup-lab/service.py
test -e /root/work/test-lifespan-cleanup-lab/test_service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/test_service.py /root/work/test-lifespan-cleanup-lab/test_service.py
cd /root/work/test-lifespan-cleanup-lab

不要把可变列表作为全局变量或默认参数来共享。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/01-contract.sh 确认。

拒绝重复启动——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:start(resource) 在资源已经打开时抛出 ValueError,否则把 open 改为 True,并在 events 中追加 'open'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

拒绝“启动两次而丢掉其中一个资源”的行为。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/02-contract.sh 确认。

让关闭具备幂等性——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:stop(resource) 仅在资源处于打开状态时,才把 open 改为 False,并在 events 中追加 'close'。如果已经关闭,则保持原样。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

即使多条清理路径重叠,也不应出现重复的 close 事件。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/03-contract.sh 确认。

禁止使用已关闭的资源——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:read(resource) 在资源已关闭时抛出 RuntimeError,处于打开状态时返回 {ready:True}。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

就绪状态与对象是否存在是两回事。对象存在,也可能是关闭的。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/04-contract.sh 确认。

在异常路径中使用 finally——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:scope(resource) 是一个 contextmanager。进入时执行 start,在块内 yield 出 resource,无论块成功还是失败,都用 stop 关闭。块内的异常要继续向上传播。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

如果只在 yield 之后写 close,一旦出现异常,就到不了那一行。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/05-contract.sh 确认。

把应用生命周期与资源连接起来——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:lifespan_for(resource) 返回 asynccontextmanager 函数 lifespan(app)。它在 scope(resource) 内设置 app.state.resource 并 yield。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

不是调用 lifespan 函数本身,而是把它传给 FastAPI 构造函数。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/06-contract.sh 确认。

通过真实请求读取就绪状态——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:create_app(resource) 使用 lifespan_for。GET /ready 返回对 app.state.resource 执行 read 的结果。context 结束时必须关闭资源。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

必须使用 with TestClient,才会同时执行 lifespan 的启动和关闭。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/07-contract.sh 确认。

请求之后的失败也要清理——测试

在 /root/work/test-lifespan-cleanup-lab/test_service.py 中测试所提供 service.py 的下列公开契约:exercise(resource, fail=False) 在 with TestClient(create_app(resource)) 内调用 GET /ready。如果 fail=True,就在其中抛出 RuntimeError,否则返回响应 JSON。两种情况下资源都必须被关闭。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

把正常路径和异常路径纳入同一种清理结构,就能减少遗漏的关闭路径。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

保存后用 bash /opt/lab/checks/test-lifespan-cleanup-lab/08-contract.sh 确认。