资讯详情

Hypothesis 策略适配指南:用 map、filter 与 assume 精确控制测试数据生成

📅 2026/9/25 4:09:39 | 华诺云谱 👁 阅读
Hypothesis 策略适配指南:用 map、filter 与 assume 精确控制测试数据生成
测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载本篇指南聚焦 HypothesisPython 属性基测试库中适配策略adapting strategies的三种核心手段——用.map内联变换生成值、用.filter过滤掉不符合条件的输入、用assume跳过整个测试用例。文章以官方教程 adapting-strategies.rst 为骨架结合仓库源码剖析这三者在底层的工作原理与取舍读完你将能根据实际测试需求写出数据生成精准、拒绝率低、运行高效的given测试。为什么需要适配策略Hypothesis 的内置策略已经提供了丰富的取值控制例如integers(min_value0)只生成非负整数integers(100, 200)只生成 100 到 200 之间的整数。但在真实测试中你往往需要比这更精细的控制想生成排序后的列表而不是任意乱序的列表想让除数和被除数之间满足某种跨策略的关系如整除想剔除少数无意义甚至导致崩溃的输入如b 0。这时就需要在已有策略之上做一层适配要么变换map要么过滤filter / assume。这三者正是本指南的主角。用.map变换策略生成的输入基本用法有时你只想对策略生成的每个值施加一个简单变换。例如lists(integers())可以生成整数列表但如果你需要的是已排序的列表直接加一个内联的.map(sorted)即可 lists(integers()).map(sorted).example() [-25527, -24245, -93, -70, -7, 0, 39, 65, 112, 6189, 19469, 32526, 1566924430]一般地strategy.map(f)返回一个新的策略它生成的所有值都会先经过函数f变换后再返回。底层实现中.map会把原策略包装成MappedStrategy见 strategies.py该类的参数分布完全继承自被包装策略变换函数pack在每次生成时被调用def map(self, pack: Callable[[Ex], T]) - SearchStrategy[T]: Returns a new strategy which generates a value from this one, and then returns pack(value). For example, integers().map(str) could generate str(5) 5. if is_identity_function(pack): return self # 恒等函数直接返回原策略避免无谓包装 return MappedStrategy(self, packpack)源码位置strategies.py值得注意的两点工程细节恒等变换零开销如果传入的pack被识别为恒等函数如lambda x: x.map会直接返回原策略本身不产生任何包装。惰性策略同样支持通过lazy()或deferred()定义的策略其.map被记录为变换列表在真正解析时才应用见 lazy.pyrepr 也会拼接成strategy.map(func)的可读形式。进阶链式变换.map返回的仍是策略因此可以继续叠加其它适配操作形成管道式的数据流水线。例如先生成非负整数、再转成二进制字符串表示from hypothesis import given, strategies as st given(st.integers(min_value0, max_value255).map(bin).map(str.upper)) def test_binary_repr(x): assert x.startswith(0B)注意.map适合一一对应的纯变换。如果变换后需要进一步控制取值范围例如把任意整数映射成日期再筛掉周末就应配合下面的.filter使用。用.filter过滤掉不想要的输入许多内置策略本身就能限定取值但有时你需要的约束超出了它们提供的参数范围这时只需过滤掉少数坏情况。一个真实的失败案例模运算测试假设我们写了一则关于取模运算的简单测试from hypothesis import given, strategies as st given(st.integers(), st.integers()) def test_remainder_magnitude(a, b): # the remainder after division is always less than # the divisor assert abs(a % b) abs(b)Hypothesis 会很快报告失败ZeroDivisionError: integer modulo by zero。和除法一样模运算在b 0时未定义。b 0这个输入对测试毫无意义我们希望把它剔除。最推荐的做法是使用.filter方法from hypothesis import assume, given, strategies as st given(st.integers(), st.integers().filter(lambda n: n ! 0)) def test_remainder_magnitude(a, b): # b is guaranteed to be nonzero here, thanks to the filter assert abs(a % b) abs(b)这段测试现在可以干净地通过。调用.filter会返回一个应用了该过滤条件的新策略例如integers().filter(lambda n: n ! 0)就是一个只生成非零整数的策略。从源码看连续多次.filter会被扁平化合并为单个FilteredStrategy内部以条件元组flat_conditions的形式统一求值而非层层嵌套见 strategies.py。过滤的重试与放弃机制.filter并不是简单的拒绝采样直到成功源码中的do_filtered_draw给出了明确的重试预算def do_filtered_draw(self, data: ConjectureData) - Ex | UniqueIdentifier: for i in range(3): value data.draw(self.filtered_strategy) failing next( (cond for cond, location in zip(...) if not cond(value)), None, ) if failing is None: return value # 记录本次被拒绝的过滤器用于观察与报错定位 data._last_rejected_filter failing return filter_not_satisfied源码位置strategies.py也就是说Hypothesis 会先重试最多 3 次如果 3 次都满足不了过滤器才把整个测试用例标记为无效mark_invalid见 data.py并附上类似Aborted test because unable to satisfy ...的提示。相较assume的一票否决.filter更宽容这也是官方建议优先使用它的原因之一。过滤器重写把过滤变成采样约束.filter更关键的性能优势在于简单的过滤器会被 Hypothesis 重写为策略本身的参数约束从而跳过无效值的生成而不是反复生成→丢弃。Hypothesis 内部维护了一套 AST 级别的谓词分析器见 filtering.py。例如lambda n: n ! 0、lambda x: x 0、lambda x: x 10这类对参数的单变量比较会被解析成min_value/max_value/exclude_min/exclude_max等约束见numeric_bounds_from_ast、get_numeric_predicate_bounds等函数再合并回底层整数策略的取值区间 lambda x: x 0 {min_value: 0}, None lambda x: x 10 {max_value: 10, exclude_max: True}, None摘自 filtering.py 的 docstring这意味着integers().filter(lambda n: n ! 0)这类写法在底层几乎等价于integers(min_value1)与integers(max_value-1)的并集——生成器直接从合法区间采样拒绝率趋近于零缩小shrinking行为也更稳定。这就是文档中所说Hypothesis 可以常把简单过滤器重写为比拒绝采样更高效的采样方法的底层原理。用assume跳过整个测试用例.filter只能约束单个策略当过滤条件涉及整个测试用例例如多个参数之间的复杂关系时Hypothesis 提供了assume函数。用法与语义assume(condition)会在condition为False时跳过当前测试用例而且可以在测试函数中的任意位置调用——不必像.filter那样提前到策略定义处。之前取模的例子用assume同样可以表达from hypothesis import assume, given, strategies as st given(st.integers(), st.integers()) def test_remainder_magnitude(a, b): assume(b ! 0) # b will be nonzero here assert abs(a % b) abs(b)从源码看assume在条件不满足时会抛出内部异常UnsatisfiedAssumption见 control.py 与 errors.pyHypothesis 捕获后将该用例标记为无效而非失败并统计到无效率中def assume(condition: object) - Literal[True]: Calling assume is like an :ref:assert python:assert that marks the |test case| as bad, rather than failing the test. This allows you to specify properties that you *assume* will be true, and let Hypothesis try to avoid similar test cases in future. ... if not condition: raise UnsatisfiedAssumption(...) return Trueassumevs.filter如何选择官方建议很明确能写进.filter的优先用.filter。.filter可以触发上文所述的过滤器重写把约束下沉为采样参数效率更高.filter会重试若干次最多 3 次而不是立刻废弃整个用例无效率更低。只有当关系无法用单个策略的过滤器表达时才退而使用assume。下面是一个需要同时过滤两类输入的例子我们希望b非零、且b能整除afrom hypothesis import assume, given, strategies as st given(st.integers(), st.integers()) def test_floor_division_lossless_when_b_divides_a(a, b): # we want to assume that: # * b is nonzero, and # * b divides a assert (a // b) * b a可以先两个条件都用assumefrom hypothesis import assume, given, strategies as st given(st.integers(), st.integers()) def test_floor_division_lossless_when_b_divides_a(a, b): assume(b ! 0) assume(a % b 0) assert (a // b) * b a然后注意到b ! 0是只依赖单个参数的约束可以下沉到策略定义里用.filter表达from hypothesis import assume, given, strategies as st given(st.integers(), st.integers().filter(lambda n: n ! 0)) def test_floor_division_lossless_when_b_divides_a(a, b): assume(a % b 0) assert (a // b) * b a而a % b 0表达了a与b之间的跨参数关系.filter无能为力必须留在assume里。这就是官方简单约束用 filter、复杂关系用 assume的完整落地示范。assumevs 提前 return一个隐蔽的陷阱另一种规避除零错误的写法是提前returnfrom hypothesis import assume, given, strategies as st given(st.integers(), st.integers()) def test_remainder_magnitude(a, b): if b 0: # bad plan - test passes without checking anything! return assert abs(a % b) abs(b)虽然这同样避免了除零但语义完全不同是个坏主意用assume时Hypothesis 知道该用例被过滤掉了不会计入max_examples次数预算也不会被当作一次通过后续引擎会自动寻找更可能满足假设的生成方向。提前return会被当作一次正常通过的测试断言一行都没执行却白占了一次max_examples。在更复杂的情况下这会让你实际检验的代码量远少于预期——大量用例被悄悄丢弃而 Hypothesis 对此毫无察觉。从引擎源码看被assume拒绝的用例走Status.INVALID路径当无效率过高时引擎会以max_iterations提前退出并给出明确提示settings.max_examples{s.max_examples}, but 1% of test cases satisfied assumptions见 engine.py。这条信息本身就是排查假设过严的第一信号——如果出现该退出原因说明过滤条件过于苛刻需要放宽假设或改用更贴近需求的策略。另外assume可以在任意嵌套深度内调用包括辅助函数内部而提前return只能放弃当前函数剩余部分无法从深层调用中整体放弃一个用例。综合建议与最小实践清单把本文的三条主线整理成可直接照抄的实践准则纯变换用.map需要把生成值转成另一种形态排序、取绝对值、转字符串等用strategy.map(f)变换应是确定性的纯函数。单策略取值约束用.filter约束只依赖单个参数且能被 AST 重写如n ! 0、x 0写进.filter会得到接近原生参数约束的生成效率还自带 3 次重试缓冲。跨参数 / 整体用例约束用assume涉及多个参数的关系整除、互质、有序性或需要在测试中途、深层函数里放弃用例用assume(condition)它不计入max_examples但过多调用会触发max_iterations退出提示。不要用提前return代替assume提前返回会把无效用例算作通过悄悄稀释测试覆盖率。围绕这些 API 的完整教程脉络见 tutorial/index.rstadapting-strategies是继builtin-strategies之后的第三篇教程后续的 custom-strategies.rst 会进一步教你如何用composite与.flatmap组合出更复杂的自定义策略而.map/.filter/assume的完整 API 文档位于 reference/api.rst。实际使用中若需要观察过滤器命中情况可开启 Hypothesis 的观察功能FilteredStrategy会记录每次因过滤器重试的事件见 strategies.py帮助你判断过滤条件是否过严。赞分享测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载相关推荐PUERTS Unity 生成器 Filter 详解用 [Filter] 与 BindingMode 精确控制 StaticWrapper 生成与 JS 访问权限PUERTS Unity 生成器 Filter 详解用 Filter 与 BindingMode 精确控制 StaticWrapper 生成与 JS 访问权限游戏开发跨平台Zact为NextJS Server Actions带来类型安全与验证的终极解决方案Zact为NextJS Server Actions带来类型安全与验证的终极解决方案 在NextJS 13的时代Server Actions为开发者提供了UnifiedDiffOutputBuilderDataProvider测试数据生成策略UnifiedDiffOutputBuilderDataProvider测试数据生成策略 在软件开发中测试是保证代码质量的关键环节而高质量的测试数据则是有开发工具测试上一篇Cloud Native Landscape中的安全会议与活动指南下一篇react-native-router-flux多栈路由实现管理复杂应用导航创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑