实战指南:基于 RuleBasedStateMachine 的操作序列级测试)
测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载导读本篇指南围绕 Hypothesis 的状态化测试stateful testing展开这是 Hypothesis 在given之外提供的一种更高级的测试范式不止生成输入数据而是让 Hypothesis 自动生成一整段操作序列去搜索能够触发系统故障的“程序”。你将掌握规则状态机Rule-Based State Machine的完整写法——包括rule、initialize、precondition、invariant、Bundle与consumes等核心构件并学会如何把状态机接入 pytest / unittest、如何定制settings、如何阅读与复现失败输出。文中示例与原理均以当前仓库 hypothesis/src/hypothesis/stateful.py 及 hypothesis/docs/stateful.rst 为准。一、什么是状态化测试让 Hypothesis 生成整个测试使用given时测试的主体仍由你编写Hypothesis 只是提供数据而使用状态化测试时Hypothesis 尝试生成的不只是数据而是整个测试。你只需声明一组可组合的“原子动作”规则Hypothesis 会努力寻找这些动作的某种组合序列使系统进入失败状态。一个典型的应用场景是你的被测系统是一个随时间变化的状态机数据库、缓存、状态服务等单次独立输入无法暴露问题而一连串互相作用的操作却能触发缺陷。你可能并不需要状态化测试状态化测试的核心思想是让 Hypothesis 同时替你的测试选择动作与取值而状态机正是实现这一点的声明式方式。但对于较简单的场景一个标准的given测试可能已经足够——你完全可以在分支或循环里使用st.data动态取值。事实上状态机探测器state machine explorer在内部也正是这样工作的在 stateful.py 中状态机测试被包装在given(st.data())之上见下文源码解析。只有当工作负载足够复杂、更高层的 API 才能发挥威力时才需要继续使用状态机。二、规则状态机Rule-Based State Machines的核心概念规则状态机与普通的given测试非常相似它从策略中取值并传给用户自定义的测试函数函数内部可用断言校验系统行为。关键区别在于given测试的各次调用必须相互独立而规则可以被链式组合——单次测试运行可能包含多次规则调用这些调用会以各种方式相互作用。规则可以接受普通的策略参数但普通策略除runner与st.data外无法感知状态机的当前状态。这正是 Bundle 存在的意义。Bundle让数据在规则之间流动:class:hypothesis.stateful.Bundle是一组命名的、可被后续操作复用的生成值集合。它由规则的结果填充也可以作为规则的参数被抽取从而让数据从一条规则流向另一条规则让规则作用于先前计算或动作的结果之上。具体规则为规则指定targeta_bundle该规则的返回值会被加入该 Bundle规则以an_argumenta_bundle作为策略会从该 Bundle 中抽取一个值规则以an_argumentconsumes(a_bundle)会从该 Bundle 中抽取一个值并将其移除consumes由 stateful.py 实现。Bundle 与普通策略一样支持.filter()与.map()并且这种变换与consumes可以以任意顺序组合consumes(a_bundle.filter(fn))与consumes(a_bundle).filter(fn)等价——两者都是抽取一个当前满足条件的值且只移除这个实际被抽中的值。在源码层面Bundle通过拦截.filter/.map见 stateful.py将变换记录在_transformations元组中使过滤与映射在同一次抽取内完成从而保证被消费的值始终是实际被抽中的那个。Bundle 与实例变量的取舍两者都能表示规则可以操作的状态。若你不需要抽取“依赖机器状态的值”直接用实例变量即可若需要抽取依赖状态的值Bundle 是最直白的方案若需要更复杂的状态相关抽取可以放弃 Bundle改用runner()与.flatmap访问实例——策略runner().flatmap(lambda self: sampled_from(self.a_list))会从实例变量a_list中抽取若还需要更复杂的行为可以用st.data基于实例或其他任意位置按规则内逻辑抽取数据。完整实战示例对比两种数据库实现下面这个示例来自 stateful.rst是 Hypothesis 自带的示例数据库example database测试的简化版本。示例数据库把键映射到值集合测试将真实实现与一个用 Pythondict模拟的“内存模型”做对拍对两者执行相同的操作序列寻找行为差异。import shutil import tempfile from collections import defaultdict import hypothesis.strategies as st from hypothesis.database import DirectoryBasedExampleDatabase from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule class DatabaseComparison(RuleBasedStateMachine): def __init__(self): super().__init__() self.tempd tempfile.mkdtemp() self.database DirectoryBasedExampleDatabase(self.tempd) self.model defaultdict(set) keys Bundle(keys) values Bundle(values) rule(targetkeys, kst.binary()) def add_key(self, k): return k rule(targetvalues, vst.binary()) def add_value(self, v): return v rule(kkeys, vvalues) def save(self, k, v): self.model[k].add(v) self.database.save(k, v) rule(kkeys, vvalues) def delete(self, k, v): self.model[k].discard(v) self.database.delete(k, v) rule(kkeys) def values_agree(self, k): assert set(self.database.fetch(k)) self.model[k] def teardown(self): shutil.rmtree(self.tempd) TestDBComparison DatabaseComparison.TestCase这里声明了两个 Bundle——一个装键、一个装值。两条琐碎的规则只负责填充数据三条非琐碎规则中save在键下保存值、delete从键下删除值两者都同步更新“应该是什么”的内存模型values_agree则针对某个键校验数据库内容与模型一致。为什么这里要用 Bundle 而非直接生成虽然不用 Bundle、直接在save和delete里生成键值也能简化代码但使用 Bundle 会鼓励 Hypothesis 在多次操作中复用相同的键和值。Bundle 操作建立了一个供各规则使用的键值“宇宙”更容易构造出具有连锁效应的失败序列。集成到测试套件TestCase可以通过unittest.TestCase把状态机接入测试套件TestTrees DatabaseComparison.TestCase # 或者直接依靠 pytest 对 unittest 的内建支持运行 if __name__ __main__: unittest.main()TestCase是RuleBasedStateMachine上的一个类属性见 stateful.py其底层是_to_test_case()动态生成的一个unittest.TestCase子类runTest内部调用run_state_machine_as_test(cls, settingsself.settings)并带有默认的settings(deadlineNone, suppress_health_checklist(HealthCheck))。失败时输出一个“可复现小程序”该测试当前可以通过但假如把self.model[k].discard(v)这行注释掉在 pytest 下运行会看到如下输出AssertionError: assert set() {b} ------------ Hypothesis ------------ state DatabaseComparison() var1 state.add_key(kb) var2 state.add_value(vvar1) state.save(kvar1, vvar2) state.delete(kvar1, vvar2) state.values_agree(kvar1) state.teardown()注意它打印出的是一段非常短的“程序”足以复现问题。规则状态机的输出通常已经非常接近合法 Python 代码——如果你为对象自定义了不产生合法 Python 的repr输出可能无法直接执行但大多数情况下你可以把这段输出原样复制粘贴进测试即可复现。从源码看输出由_repr_step负责生成stateful.py它会为带target的规则生成var1 state.add_key(...)形式的变量赋值并用VarReference引用先前步骤的结果。用settings精细控制行为你可以通过TestCase上的settings对象控制行为细节这是一个普通的 Hypothesis settings 对象使用TestCase类首次被引用时生效的默认值。例如希望运行更少的用例、每个用例跑更长的操作序列DatabaseComparison.TestCase.settings settings( max_examples50, stateful_step_count100 )这使每个程序的步数翻倍stateful_step_count100 步同时将运行的测试用例数量减半max_examples50 个。三、Rules规则的完整约束规则是RuleBasedStateMachine中最常用的构件通过给函数应用rule装饰器定义stateful.py。使用时有几条硬性约束状态机必须至少定义一个规则否则实例化时抛出InvalidDefinition提示State machine X defines no rules见 stateful.py一个函数不能用于定义多个规则这是为了避免多个规则做同一件事——对同一函数重复应用rule会抛出InvalidDefinition由于状态机的执行机制规则一般不能从其他来源如 fixture 或pytest.mark.parametrize取参数——考虑改用sampled_from之类的策略来提供候选值rule与invariant/initialize互斥同一函数不能同时被多种装饰器修饰若规则没有指定target函数必须返回None否则触发HealthCheck.return_value健康检查stateful.py。规则可以把返回值同时送入多个 Bundle传targets(a, b)而非targeta。若希望把结果恰好送入多个 Bundle 之一则为每种情况分别定义规则。源码中的_convert_targetsstateful.py会校验 target 必须为Bundle并会拦截one_of(a, b)/a | b这类误用。需要向 target 返回多个结果时使用multiple(result1, result2, ...)stateful.pymultiple()空调用则用于无结果地结束规则。四、Initializes初始化规则initialize是规则的一种特殊情形保证在任何普通规则之前恰好运行一次。若定义了多个 initialize 规则它们都会被调用但顺序任意且每次运行顺序都可能变化stateful.py 中源码明确initialize方法在每次运行中恰好被调用一次除非某个初始化抛出了异常——之后只执行teardown。initialize规则不允许带有precondition源码会直接抛出InvalidDefinition。初始化规则最常见的用途是填充 Bundleimport hypothesis.strategies as st from hypothesis.stateful import Bundle, RuleBasedStateMachine, initialize, rule name_strategy st.text(min_size1).filter(lambda x: / not in x) class NumberModifier(RuleBasedStateMachine): folders Bundle(folders) files Bundle(files) initialize(targetfolders) def init_folders(self): return / rule(targetfolders, parentfolders, namename_strategy) def create_folder(self, parent, name): return f{parent}/{name} rule(targetfiles, parentfolders, namename_strategy) def create_file(self, parent, name): return f{parent}/{name}初始化规则还可以用于“依赖策略取值地初始化被测系统”例如在状态机中放一个“系统是否已初始化”的实例变量再借助前置条件见下节保证在任何依赖初始化的规则运行前恰好有一个负责初始化的规则被执行过。五、Preconditions前置条件在规则里使用hypothesis.assume是可行的但如果只有少数几条规则使用了假设很容易出现“几乎没有规则能通过假设”的窘境。为此 Hypothesis 提供了precondition装饰器stateful.py它作用于rule装饰过的函数接收一个基于RuleBasedStateMachine实例返回 True/False 的函数from hypothesis.stateful import RuleBasedStateMachine, precondition, rule class NumberModifier(RuleBasedStateMachine): num 0 rule() def add_one(self): self.num 1 precondition(lambda self: self.num ! 0) rule() def divide_with_one(self): self.num 1 / self.num使用precondition而非assumeHypothesis 可以在运行规则之前就过滤掉不适用的规则从而大大提高生成有用步骤序列的概率。规则选择阶段会先做可用性判定RuleStrategy.is_valid见 stateful.py前置条件全部为 True 且依赖的 Bundle 非空规则才算可选。若某一步没有任何规则满足前置条件会抛出InvalidDefinition提示No progress can be made from state ..., because no available rule had a True precondition。需要注意两点前置条件目前不能访问 Bundle需要用到前置条件时请把相关数据存放在实例上多条precondition可以叠加到同一条规则上只有当全部返回 True 时规则才被视为有效precondition同样可以作用于invariant。单独使用precondition未配合rule或invariant是非法的源码会直接报错stateful.py。六、Invariants不变量很多时候我们想保证某个不变量在过程的每一步之后都成立。把它写成规则虽然可行但它会在其他规则之间被运行零次或多次。Hypothesis 提供的invariant装饰器stateful.py把函数标记为每一步之后都要运行from hypothesis.stateful import RuleBasedStateMachine, invariant, rule class NumberModifier(RuleBasedStateMachine): num 0 rule() def add_two(self): self.num 2 if self.num 50: self.num 1 invariant() def is_even(self): assert self.num % 2 0 NumberTest NumberModifier.TestCase细节补充不变量也可以应用precondition此时仅当前置条件返回 True 时才运行不变量目前不能访问 Bundle需要时同样应把相关数据存到实例上默认情况下不变量只会在所有initialize规则执行完毕之后才开始逐步骤检查若希望初始化期间也检查可传入invariant(check_during_initTrue)不变量函数必须返回None返回其他值会触发HealthCheck.return_value健康检查stateful.py不变量不能与rule/initialize混用在同一个函数上。七、更细粒度的控制run_state_machine_as_test如果不想走TestCase这套基础设施可以手动调用状态机测试。stateful模块暴露了run_state_machine_as_test(state_machine_factory, *, settingsNone, _min_steps0)stateful.pystate_machine_factory任意“无参调用即返回一个RuleBasedStateMachine实例”的对象——可以是类也可以是函数settings控制测试执行的 settings 对象可选。它等价于基于类的runTest所做的事情静默运行或在找到最小失败程序时打印出来并抛出异常。此外若在选择规则阶段抛出了FlakyStrategyDefinitionHypothesis 会在异常上附加说明——“通常是飘忽不定的前置条件flaky precondition或意外为空的 Bundle 导致的”。八、源码级解析状态机在内部是如何运行的要深入理解以上行为的原理关键看 hypothesis/src/hypothesis/stateful.py 的三处实现。1. 状态机测试本质上是given(st.data())驱动的get_state_machine_teststateful.py把状态机测试包装成settings given(st.data()) def run_state_machine(data): machine state_machine_factory() ... max_steps settings.stateful_step_count while True: # 选择规则 - 生成参数 - 执行规则 - 检查不变量 ...这正是文档开头“状态机探测器内部就是靠st.data实现的”这句话的代码形态。循环内部先优先运行尚未执行过的 initialize 规则machine._initialize_rules_to_run再通过RuleStrategy随机选择普通规则每轮执行后调用machine.check_invariants(...)检查不变量最终统一输出state.teardown()并调用machine.teardown()stateful.py。teardown默认什么都不做可在子类中覆写以清理资源。步数控制也在这里max_steps settings.stateful_step_count为了便于收缩正常运行时每一步都有2 ** -16的概率提前终止达到上限后强制停止。2. 规则选择RuleStrategy与特性标志RuleStrategystateful.py负责在每一步选出要执行的规则。它先用FeatureStrategy随机“启用”一批规则再在启用且可用的规则中用sampled_from抽取。规则的可用性判定is_valid同时检查两件事规则用到的每个 Bundle 非空Bundle.do_draw从空 Bundle 抽取会调用data.mark_invalid提示Cannot draw from empty bundle见 stateful.py以及全部前置条件为 True。注意过滤顺序是有意设计的先判断可用性、再判断是否启用以避免生成过大的 choice sequence。3.stateful_step_count与max_examples的乘法关系在 hypothesis/src/hypothesis/_settings.py 中stateful_step_count被定义为“在放弃寻找缺陷之前最多调用多少次额外的rule方法”默认值为50文档明确说明它与max_examples是乘法关系——每个测试用例最多运行stateful_step_count步因此总执行量约为二者之积max_examples默认值为100。这就是“把步数翻倍、用例数减半”的配置为何把执行总量基本维持在同一量级。4. 仓库中的验证测试仓库的测试套件覆盖了状态机的各种边界情况可以作为继续深入阅读的入口hypothesis/tests/cover/test_database_backend.py文档示例的现实版本直接以run_state_machine_as_test(TestDatabaseListener)手动运行状态机L696hypothesis/tests/cover/test_health_checks.py验证无 target 的规则、initialize、invariant返回非None值时触发的健康检查hypothesis/tests/cover/test_error_in_draw.py验证参数生成阶段抛错的处理hypothesis/tests/cover/test_flakiness.py覆盖与状态相关的飘忽不定flaky场景。九、小结状态化测试把 Hypothesis 的能力从“生成输入”提升到“生成程序”通过RuleBasedStateMachine声明一组规则与 BundleHypothesis 会自动组合出具有连锁效应的操作序列、搜索最小失败序列并输出可直接复现的 Python 代码。实践中记住几条关键规则Bundle 负责让数据在规则间流动、initialize负责一次性前置填充、precondition负责过滤不适用的步骤、invariant负责每一步之后的不变量检查用TestCase.settings而非类属性赋值控制stateful_step_count与max_examples的步数/用例数配比需要脱离TestCase时直接用run_state_machine_as_test。更深的运行机制与边界行为可继续阅读 hypothesis/src/hypothesis/stateful.py、hypothesis/src/hypothesis/_settings.py 以及上述测试文件。赞分享测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载相关推荐Terraformer自动连接资源教程terraform_remote_state引用原理与--connect详解Terraformer自动连接资源教程terraform_remote_state引用原理与 connect详解 Terraformer 是一款反向 Terr开发工具CLIIaCDevOpsopencode-anthropic-auth的User-Agent伪装深度剖析为何必须impersonate claude-cliopencode anthropic auth的User Agent伪装深度剖析为何必须impersonate claude cli opencode antHypothesis高级特性状态机和规则测试Hypothesis高级特性状态机和规则测试 Hypothesis的RuleBasedStateMachine是状态机测试的核心组件提供了一种结构化方式来定测试开发工具上一篇老Mac升级macOSOCLP 2.5.0 一次跑通下一篇Metallb文档本地化指南翻译工具与质量控制流程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考