如何对带有volatile output的函数执行doctest测试?
你遇到的这个问题特别常见——当函数的输出包含随机值、当前时间、动态生成的内容这类每次运行都会变化的元素时,doctest的严格字符串匹配就会直接失效。下面给你几个尽量只在docstring内完成测试的解决方案,不会额外增加外部测试代码影响覆盖率:
1. 用ELLIPSIS标记忽略可变片段
doctest自带的+ELLIPSIS指令允许你用...匹配任意长度的任意内容,完美用来跳过那些每次都不一样的部分。比如你的函数返回当前时间:
import datetime def get_current_time(): """返回当前的ISO格式时间 >>> get_current_time() # doctest: +ELLIPSIS '2024-...T...:...:...' """ return datetime.datetime.now().isoformat()
这里...会匹配时间中的月份、日、时分秒等可变部分,只要输出的格式框架正确,测试就能通过。你可以根据实际输出调整...的位置,比如写成'...T...'来匹配任意ISO时间格式。
2. 验证输出的属性而非具体值
如果不需要精确匹配输出内容,而是要确保输出符合预期的规则(比如范围、类型、格式),可以在docstring的测试代码里直接做断言。比如生成随机数的函数:
import random def generate_random_score(): """返回0到100之间的随机整数分数 >>> score = generate_random_score() >>> isinstance(score, int) True >>> 0 <= score <= 100 True """ return random.randint(0, 100)
这种方式完全不依赖具体的输出值,只验证输出的特性,稳定性拉满,而且所有代码都在docstring里,不会额外增加测试文件。
3. 临时mock易变依赖
如果函数的易变输出来自某个外部依赖(比如系统时间、随机数生成器),可以在docstring里用unittest.mock临时替换这个依赖,固定它的返回值。比如:
import datetime def get_today_str(): """返回今天的日期字符串,格式为YYYY-MM-DD >>> from unittest.mock import patch >>> with patch('datetime.datetime') as mock_dt: ... mock_dt.now.return_value = datetime.datetime(2024, 6, 15) ... get_today_str() '2024-06-15' """ return datetime.datetime.now().strftime('%Y-%m-%d')
这里用patch把datetime.datetime.now临时替换成固定返回2024-06-15的mock对象,这样测试时输出完全可控。虽然用到了mock,但所有代码都在docstring内,不会影响覆盖率统计。
4. 万不得已时跳过测试
如果某个测试用例确实无法稳定运行(比如依赖外部API的动态内容),可以用+SKIP指令跳过,但这是最后的备选方案,尽量少用:
def fetch_external_data(): """从外部API获取动态数据 >>> fetch_external_data() # doctest: +SKIP 'dynamic external content' """ import requests return requests.get('https://example.com/api/data').text
总结一下:优先选择验证输出属性或者用ELLIPSIS忽略可变部分,其次用mock固定依赖,尽量避免跳过测试——这样既能保证测试的有效性,又能满足你"不在docstring外写测试代码"的需求。
内容的提问来源于stack exchange,提问作者Elrond

