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

测试工具实战

无需 sleep 即可重现时间边界

在 TT Lab 中继续学习

目标

用假时钟验证限制窗口、重试时刻和按用户隔离。

为什么重要

在请求限流的测试中加入 sleep 之后,开发机上能通过,CI 上却失败了。运行速度慢的执行器改变了时间边界,测试本身也要跑很久。时间是程序的输入,所以应让调用方能够控制它,并且精确踩在边界上。

步骤

  1. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:validate_limit(limit, window) 只允许除 bool 之外的正 int 作为 limit,以及正的有限 int/float 作为 window,并返回 (limit, float(window))。其余情况抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

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

mkdir -p /root/work/test-clock-window-lab
test -e /root/work/test-clock-window-lab/service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/service.py /root/work/test-clock-window-lab/service.py
test -e /root/work/test-clock-window-lab/test_service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/test_service.py /root/work/test-clock-window-lab/test_service.py
cd /root/work/test-clock-window-lab
  1. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:active(history, now, window) 只返回大于 now-window 的时刻,并以原有顺序组成新列表。history 是已排序的非递减时刻。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  2. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:retry_after(history, now, window) 对已清理过的非空 history,返回“第一个时刻+window-now 的 ceil 值”与 0 中较大的整数。空列表返回 0。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  3. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:history_for(state, key) 在键不存在时返回空列表,存在时返回该记录的副本。仅仅查询不会修改 state。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  4. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:admit(state, key, now, limit, window) 在验证配置之后,清理该键已过期的记录。有余量就追加 now 并返回 (True,0),已满则不追加并返回 (False,retry_after)。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  5. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:client_key(value) 对由 1–40 个 ASCII 字母、数字、连字符组成的字符串原样返回,其余抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  6. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:limited_response(wait) 返回一个 JSONResponse:状态 429,正文 {error:'rate_limited'},Retry-After 请求头为把 wait 转成字符串后的值。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

  7. 在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:create_app(clock, limit=2, window=10) 在 GET /work 上检查 X-Client-ID:无效的键返回 400 {error:'invalid_client'},允许时返回 200 {ok:True},超出时返回 limited_response。state 按应用分开。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

参考

验证配置——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:validate_limit(limit, window) 只允许除 bool 之外的正 int 作为 limit,以及正的有限 int/float 作为 window,并返回 (limit, float(window))。其余情况抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

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

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

bool 是 int 的子类型。NaN 和无穷大也必须单独拒绝。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

排除窗口的左边界——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:active(history, now, window) 只返回大于 now-window 的时刻,并以原有顺序组成新列表。history 是已排序的非递减时刻。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

确认把刚好过期的时刻保留下来的 >= 与 > 之间的差别。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

对等待时间向上取整——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:retry_after(history, now, window) 对已清理过的非空 history,返回“第一个时刻+window-now 的 ceil 值”与 0 中较大的整数。空列表返回 0。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

如果还剩 0.2 秒却把 Retry-After 设为 0,客户端会立即重新请求。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

按键分开记录——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:history_for(state, key) 在键不存在时返回空列表,存在时返回该记录的副本。仅仅查询不会修改 state。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

如果返回共享列表,一个请求的清理就可能改掉另一个请求的记录。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

只记录被允许的请求——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:admit(state, key, now, limit, window) 在验证配置之后,清理该键已过期的记录。有余量就追加 now 并返回 (True,0),已满则不追加并返回 (False,retry_after)。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

如果把被拒绝的请求也追加进去,每次重试都会把过期时刻往后推。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

验证客户端键——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:client_key(value) 对由 1–40 个 ASCII 字母、数字、连字符组成的字符串原样返回,其余抛出 ValueError。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

限制输入的范围,避免无限制的键大小挤占状态内存。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

构造拒绝响应——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:limited_response(wait) 返回一个 JSONResponse:状态 429,正文 {error:'rate_limited'},Retry-After 请求头为把 wait 转成字符串后的值。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

同时发送状态和请求头,让客户端知道重试的时间。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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

用虚拟时间走完请求流程——测试

在 /root/work/test-clock-window-lab/test_service.py 中测试所提供 service.py 的下列公开契约:create_app(clock, limit=2, window=10) 在 GET /work 上检查 X-Client-ID:无效的键返回 400 {error:'invalid_client'},允许时返回 200 {ok:True},超出时返回 limited_response。state 按应用分开。在正确实现上应当通过,而在违反该契约的实现上,必须由测试本体的实际失败将其检出。保留前面步骤的测试,并添加 test_ 函数。

不要真的 sleep,而是把放进列表的当前时刻通过 clock 函数传入。不要修改实现文件。用 pytest.raises 确认预期的异常,对正常结果则断言具体的预期值。

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