防止错误响应回归的契约测试
目标
测试已知错误、未知错误、非公开信息和追踪请求头。
为什么重要
错误消息被修改之后,移动端 App 的重试逻辑坏掉了。测试只确认了“抛出了异常”这一事实,并没有比较状态和公开正文的含义。内部异常原样暴露给用户的回归,也从同一个缺口溜了过去。
步骤
- 在
/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
-
在
/root/work/test-error-contract-lab/test_service.py中测试所提供 service.py 的下列公开契约:status_for(code) 对 missing 返回 404,对 conflict 返回 409,对 invalid 返回 422,其他返回 500。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。 -
在
/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_ 函数。 -
在
/root/work/test-error-contract-lab/test_service.py中测试所提供 service.py 的下列公开契约:request_id(value) 在 value 是只由 ASCII 字母、数字、下划线和连字符组成、长度为 1–32 的字符串时原样返回,否则返回 'untracked'。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。 -
在
/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_ 函数。 -
在
/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_ 函数。 -
在
/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_ 函数。 -
在
/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_ 函数。
参考
- 无需联网,也无需安装软件包,在现有的 lab-dev 环境中进行。
- 每一步都在 45 秒的评分预算内运行。不要添加真实的 sleep 或网络调用。
- 提交的测试会在另外的临时文件夹中,分别在正确实现和有缺陷的实现上运行。在正确实现上,实际运行的测试必须全部通过;在有缺陷的实现上,测试本体必须失败。收集错误、运行 0 个、全部跳过、被强制终止都不算通过。只使用 pytest 的基本功能和提供的库。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:本实验的问题响应是带有 type、title、status、detail、request_id 的教学用契约。不主张通用的国际化,也不主张完全符合标准。request id 是用于追踪的字符串,而不是认证手段;生产日志中也不应原样记录机密值。提供的实现可以阅读,但评分使用的是另外的副本。不要通过检查源码措辞或修改文件来绕过缺陷,而要检查公开接口的实际运行结果。
在业务异常中保留 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 确认。