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

测试工具实战

防止错误响应回归的契约测试

在 TT Lab 中继续学习

目标

测试已知错误、未知错误、非公开信息和追踪请求头。

为什么重要

错误消息被修改之后,移动端 App 的重试逻辑坏掉了。测试只确认了“抛出了异常”这一事实,并没有比较状态和公开正文的含义。内部异常原样暴露给用户的回归,也从同一个缺口溜了过去。

步骤

  1. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:DomainError(code, message) 是 Exception 的子类,在 .code 中保存 code。str(异常) 就是 message。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

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

mkdir -p /root/work/test-error-contract-lab
test -e /root/work/test-error-contract-lab/service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/service.py /root/work/test-error-contract-lab/service.py
test -e /root/work/test-error-contract-lab/test_service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/test_service.py /root/work/test-error-contract-lab/test_service.py
cd /root/work/test-error-contract-lab
  1. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:status_for(code) 对 missing 返回 404,对 conflict 返回 409,对 invalid 返回 422,其他返回 500。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  2. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:public_message(code) 对 missing 返回 'Resource not found',对 conflict 返回 'State conflict',对 invalid 返回 'Invalid request',其他返回 'Internal error'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  3. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:request_id(value) 在 value 是只由 ASCII 字母、数字、下划线和连字符组成、长度为 1–32 的字符串时原样返回,否则返回 'untracked'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  4. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:problem(code, rid) 返回只含下列键的字典:type='urn:labhub:problem:'+code,title 和 detail=public_message(code),status=status_for(code),request_id=request_id(rid)。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  5. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:response_for(code, rid) 返回一个 JSONResponse:以 problem 为正文,以 status_for 为状态,以 application/problem+json 为 media_type,以规范化后的 rid 作为 X-Request-ID。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  6. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:install_handlers(app) 会注册 DomainError 的处理器。它读取请求头 X-Request-ID,并针对 exc.code 返回 response_for。exc 的 message 不会放进响应。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  7. 在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:create_app() 会安装处理器,并在 GET /fail/{code} 上抛出 DomainError(code, 内部消息)。但当 code=boom 时,抛出的是 RuntimeError。RuntimeError 的处理器返回 code=internal 的固定 500 问题响应。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

参考

在业务异常中保留 code——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:DomainError(code, message) 是 Exception 的子类,在 .code 中保存 code。str(异常) 就是 message。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

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

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

把供机器判断的 code 与供人查看的内部消息分开。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

把 code 映射为状态——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:status_for(code) 对 missing 返回 404,对 conflict 返回 409,对 invalid 返回 422,其他返回 500。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

不要把未知的 code 当作成功处理。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

固定公开文案——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:public_message(code) 对 missing 返回 'Resource not found',对 conflict 返回 'State conflict',对 invalid 返回 'Invalid request',其他返回 'Internal error'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

即使异常字符串中含有数据库地址或内部路径,也不对外公开。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

限制请求 id——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:request_id(value) 在 value 是只由 ASCII 字母、数字、下划线和连字符组成、长度为 1–32 的字符串时原样返回,否则返回 'untracked'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

同时限制长度和字符集,避免原样回显任意请求头。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

构造问题正文——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:problem(code, rid) 返回只含下列键的字典:type='urn:labhub:problem:'+code,title 和 detail=public_message(code),status=status_for(code),request_id=request_id(rid)。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

如果正文和 HTTP 状态互相不一致,客户端就无法决定该相信哪个值。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

让响应格式保持一致——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:response_for(code, rid) 返回一个 JSONResponse:以 problem 为正文,以 status_for 为状态,以 application/problem+json 为 media_type,以规范化后的 rid 作为 X-Request-ID。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

直接返回简单字典,可能会让错误也变成 200。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

接入业务异常处理器——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:install_handlers(app) 会注册 DomainError 的处理器。它读取请求头 X-Request-ID,并针对 exc.code 返回 response_for。exc 的 message 不会放进响应。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

不要把捕获异常的位置分散开,而要放在应用的公共边界上。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

连意料之外的错误也隐藏起来——测试

在 /root/work/test-error-contract-lab/test_service.py 中测试所提供 service.py 的下列公开契约:create_app() 会安装处理器,并在 GET /fail/{code} 上抛出 DomainError(code, 内部消息)。但当 code=boom 时,抛出的是 RuntimeError。RuntimeError 的处理器返回 code=internal 的固定 500 问题响应。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

在测试中关闭异常的重新抛出,检查真实 500 响应的字节。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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