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

测试工具实战

隔离配置测试中的全局状态:设计原理

在 TT Lab 中继续学习

一句话总结

验证错误的环境值、默认值、快照,以及机密字段暴露的回归。

为什么需要它

先运行的测试修改了环境变量,导致后面的测试失败。把测试顺序固定下来虽然掩盖了问题,但实际应用中连配置拼写错误都会变成正常默认值的 bug 依然存在。必须分别确认输入隔离和严格解析。

工作原理

给每个测试传入一个新的环境 dict。把环境变量不存在与存在但有误这两种情况分开,并确认 bool 字符串、端口范围和有限的 timeout。创建应用之后,修改原始输入,检查快照是否独立,并断言公开响应的键集合。

학생 테스트 → 정상 구현: 실제 시험 모두 통과
           └→ 계약 위반 구현: 해당 동작에서 실패
수집 실패·0개 실행·강제 종료 ≠ 결함 검출

阅读契约并预测失败的工作表

下面不是让你背下整个实现的答案,而是逐步骤的代码评审。每个改动片段都会故意破坏契约。请注意,改动之后正常用例仍然可能通过。运行之前先预测:观察哪些输入、异常和状态,差异才会暴露出来;实现之后,再把预测与结果进行比较。

1. 显式解析布尔值——测试

测试所提供 service.py 的下列公开契约:parse_bool(value) 仅当忽略大小写后为 true 或 false 时,才返回对应的 bool。带有空白,或不是字符串时,抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:bool('false') 的结果是 True。请直接比较允许的两个字符串。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

bool(value)

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

2. 检查端口范围——测试

测试所提供 service.py 的下列公开契约:parse_port(value) 把只含 ASCII 数字的字符串转换为 int,若在 1–65535 范围内则返回。其余抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:整数转换成功,并不意味着它是有效的端口范围。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

<= 65536

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

3. 让超时时间成为有限值——测试

测试所提供 service.py 的下列公开契约:parse_timeout(value) 把字符串转换为 float,只返回大于 0 且不超过 30 的有限值。其余抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:NaN 在普通比较中的行为与预期不同,所以要检查 isfinite。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

0 <= number <= 30

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

4. 拒绝缺失的必需机密——测试

测试所提供 service.py 的下列公开契约:required_token(env) 在 TOKEN 是字符串且 strip 之后不为空时,返回 strip 后的值。缺失或为空值时抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:不要用示例默认值顶替缺失的必需机密。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

return value

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

5. 只对缺失的项应用默认值——测试

测试所提供 service.py 的下列公开契约:load_settings(env) 返回这样的字典:service 取 env 的 SERVICE,缺失时为 'api';debug=parse_bool(DEBUG,缺失时为 'false');port=parse_port(PORT,缺失时为 '8000');timeout=parse_timeout(TIMEOUT,缺失时为 '5');token=required_token。空的 SERVICE 抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:get 的默认值与“值 or 默认值”在处理空字符串时并不相同。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

env.get("DEBUG","true")

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

6. 从公开配置中移除机密——测试

测试所提供 service.py 的下列公开契约:public_settings(settings) 返回只含 service 和 debug 的新字典。不修改原始字典。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:与其对令牌值的一部分做掩码,不如采用根本不公开该字段的契约。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

"debug":settings["debug"],"token":settings["token"]}

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

7. 把外部变更与配置分开——测试

测试所提供 service.py 的下列公开契约:snapshot(env) 返回 load_settings 的结果。调用之后即使修改 env,返回的配置也不会改变。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:把应用启动时刻的配置,与之后可能改变的输入字典分开。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

return env

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

8. 确认启动失败与公开响应——测试

测试所提供 service.py 的下列公开契约:create_app(env) 会立即读取 snapshot,配置有误时以 ValueError 使应用创建失败。GET /info 只返回 public_settings。用不同 env 创建的应用之间不共享配置。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

判断依据:在创建时就进行验证,避免服务器启动之后,配置错误要到第一个请求时才暴露。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

待评审的错误改动片段:

return settings

把它与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观察对象。

在现场相遇的样子

这里用普通字典来注入环境,所以不依赖真实的进程全局环境。这不是一个连加密的机密存储、密钥轮换和动态重新加载都实现了的示例。令牌字符串只是学习用的输入,不要把真实的生产密钥放进实验 Pod。提供的实现可以阅读,但评分使用的是另外的副本。不要通过检查源码措辞或修改文件来绕过缺陷,而要检查公开接口的实际运行结果。

下一项实验要做什么

八个步骤会连成一个可运行的成果。显式解析布尔值——测试 → 检查端口范围——测试 → 让超时时间成为有限值——测试 → 拒绝缺失的必需机密——测试 → 只对缺失的项应用默认值——测试 → 从公开配置中移除机密——测试 → 把外部变更与配置分开——测试 → 确认启动失败与公开响应——测试。

每一步检查的都不是函数或文件是否存在,而是实际的返回值、异常和状态变化。看过正确答案之后,请故意改动边界比较或清理代码,确认哪些测试会失败。说明为什么前面的测试在后面的步骤中仍然保留,并写出一种本实验不能保证的生产条件。