[{"data":1,"prerenderedAt":1283},["ShallowReactive",2],{"page-\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002F":3},{"id":4,"title":5,"body":6,"description":1236,"extension":1237,"meta":1238,"navigation":395,"path":1279,"seo":1280,"stem":1281,"__hash__":1282},"content\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002Findex.md","Stateful and Model-Based Testing",{"type":7,"value":8,"toc":1220},"minimark",[9,18,32,37,84,112,116,142,145,187,332,336,343,348,531,538,542,572,584,588,643,653,657,711,727,731,788,798,802,809,883,892,896,899,918,921,936,950,954,1073,1090,1094,1112,1129,1141,1157,1173,1177,1209,1216],[10,11,12,13,17],"p",{},"Example-based tests verify one operation at a time, but real defects in stateful systems — caches, queues, ORMs, connection pools, finite state machines — emerge only from ",[14,15,16],"em",{},"sequences"," of operations: insert then evict then read, acquire then acquire then release. Enumerating those sequences by hand is hopeless; the interesting bug is usually the eleven-step interleaving nobody wrote down. Hypothesis solves this by generating operation sequences for you, executing them against your real system and a simplified model, and shrinking any failing sequence to the shortest reproduction. The failure mode this guide targets is the intermittent \"works in isolation, breaks under a specific order\" bug that example tests structurally cannot find.",[10,19,20,21,26,27,31],{},"This guide builds on the generative model introduced in ",[22,23,25],"a",{"href":24},"\u002Fproperty-based-fuzz-testing-strategies\u002Fhypothesis-framework-fundamentals\u002F","Hypothesis Framework Fundamentals"," and the strategy composition covered in ",[22,28,30],{"href":29},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002F","Advanced Property-Based Testing",". Here the unit of generation is no longer a single value but an entire program of method calls.",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,44,63,69],"ul",{},[41,42,43],"li",{},"Python 3.9+",[41,45,46,50,51,54,55,58,59,62],{},[47,48,49],"code",{},"hypothesis >= 6.0"," (the ",[47,52,53],{},"RuleBasedStateMachine",", ",[47,56,57],{},"Bundle",", and ",[47,60,61],{},"run_state_machine_as_test"," API used below has been stable since 6.x)",[41,64,65,68],{},[47,66,67],{},"pytest >= 7.0"," for collection of the machine classes",[41,70,71,72,75,76,79,80,83],{},"Familiarity with ",[47,73,74],{},"hypothesis.strategies"," (aliased ",[47,77,78],{},"st",") and the ",[47,81,82],{},"@given"," lifecycle",[85,86,91],"pre",{"className":87,"code":88,"language":89,"meta":90,"style":90},"language-bash shiki shiki-themes github-light github-dark","pip install \"hypothesis>=6.0\" \"pytest>=7.0\"\n","bash","",[47,92,93],{"__ignoreMap":90},[94,95,98,102,106,109],"span",{"class":96,"line":97},"line",1,[94,99,101],{"class":100},"sScJk","pip",[94,103,105],{"class":104},"sZZnC"," install",[94,107,108],{"class":104}," \"hypothesis>=6.0\"",[94,110,111],{"class":104}," \"pytest>=7.0\"\n",[33,113,115],{"id":114},"core-concept","Core concept",[10,117,118,119,121,122,125,126,129,130,133,134,137,138,141],{},"A ",[47,120,53],{}," is a description of a system as a set of ",[14,123,124],{},"transitions",". Hypothesis treats the class as a generator of programs: it picks a legal rule, supplies generated arguments, executes it, checks invariants, and repeats up to ",[47,127,128],{},"stateful_step_count"," times. The \"model-based\" half of the name comes from running a deliberately simple reference implementation (often a plain ",[47,131,132],{},"dict"," or ",[47,135,136],{},"list",") alongside the real system under test, and asserting the two agree. The model encodes what ",[14,139,140],{},"should"," happen; the rules drive the real code; the invariants catch divergence.",[10,143,144],{},"Five primitives compose the whole approach:",[38,146,147,156,164,171,179],{},[41,148,149,155],{},[150,151,152],"strong",{},[47,153,154],{},"@rule"," — a candidate operation. Hypothesis may call it any number of times, in any order, with arguments drawn from the strategies you bind to its parameters.",[41,157,158,163],{},[150,159,160],{},[47,161,162],{},"@initialize"," — runs at most once, before any rule, to establish deterministic starting state.",[41,165,166,170],{},[150,167,168],{},[47,169,57],{}," — a named queue of values produced by rules, so later rules can operate on objects that earlier rules actually created (you never fabricate an ID that does not exist).",[41,172,173,178],{},[150,174,175],{},[47,176,177],{},"@invariant"," — a property checked after every rule; it asserts truths that hold in every reachable state.",[41,180,181,186],{},[150,182,183],{},[47,184,185],{},"@precondition"," — a guard that disables a rule unless the machine is in a valid state for it.",[188,189,192,328],"figure",{"className":190},[191],"diagram",[193,194,201,202,201,206,201,210,201,220,201,231,201,236,201,241,201,246,201,249,201,252,201,256,201,260,201,263,201,268,201,272,201,276,201,279,201,287,201,292,201,297,201,304,201,309,201,315,201,319,201,323],"svg",{"viewBox":195,"role":196,"ariaLabelledBy":197,"xmlns":200},"0 0 800 380","img",[198,199],"statemc-t","statemc-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[203,204,205],"title",{"id":198},"Rule-based state machine transitions",[207,208,209],"desc",{"id":199},"Initialize seeds the model, rules push and consume bundle values as transitions, and an invariant is checked after every step.",[211,212,219],"text",{"x":213,"y":214,"textAnchor":215,"fontSize":216,"fontWeight":217,"fill":218},"400","34","middle","19","700","#3d405b","RuleBasedStateMachine lifecycle",[221,222],"rect",{"x":223,"y":224,"width":225,"height":226,"rx":227,"fill":228,"stroke":229,"strokeWidth":230},"40","70","180","74","14","#fffdf8","#e07a5f","2",[211,232,162],{"x":233,"y":234,"textAnchor":215,"fontSize":235,"fontWeight":217,"fill":218},"130","102","15",[211,237,240],{"x":233,"y":238,"textAnchor":215,"fontSize":239,"fill":218},"124","12","seed model once",[221,242],{"x":243,"y":224,"width":244,"height":226,"rx":227,"fill":228,"stroke":245,"strokeWidth":230},"300","200","#81b29a",[211,247,248],{"x":213,"y":234,"textAnchor":215,"fontSize":235,"fontWeight":217,"fill":218},"@rule create",[211,250,251],{"x":213,"y":238,"textAnchor":215,"fontSize":239,"fill":218},"target = Bundle",[221,253],{"x":254,"y":224,"width":225,"height":226,"rx":227,"fill":228,"stroke":255,"strokeWidth":230},"580","#f2cc8f",[211,257,259],{"x":258,"y":234,"textAnchor":215,"fontSize":235,"fontWeight":217,"fill":218},"670","@rule mutate",[211,261,262],{"x":258,"y":238,"textAnchor":215,"fontSize":239,"fill":218},"consume Bundle",[96,264],{"x1":265,"y1":266,"x2":267,"y2":266,"stroke":218,"strokeWidth":230},"220","107","298",[269,270],"polygon",{"points":271,"fill":218},"298,107 288,102 288,112",[96,273],{"x1":274,"y1":266,"x2":275,"y2":266,"stroke":218,"strokeWidth":230},"500","578",[269,277],{"points":278,"fill":218},"578,107 568,102 568,112",[221,280],{"x":281,"y":282,"width":283,"height":284,"rx":239,"fill":285,"stroke":218,"strokeWidth":286},"240","190","320","58","#f4f1de","1.5",[211,288,291],{"x":213,"y":289,"textAnchor":215,"fontSize":290,"fontWeight":217,"fill":218},"216","13","Bundle: created resources",[211,293,296],{"x":213,"y":294,"textAnchor":215,"fontSize":295,"fill":218},"236","11.5","push on create, draw on mutate",[96,298],{"x1":213,"y1":299,"x2":213,"y2":300,"stroke":218,"strokeWidth":301,"strokeDashArray":302},"144","188","1.4",[303,303],"4",[96,305],{"x1":306,"y1":299,"x2":307,"y2":300,"stroke":218,"strokeWidth":301,"strokeDashArray":308},"640","540",[303,303],[221,310],{"x":311,"y":312,"width":313,"height":314,"rx":239,"fill":228,"stroke":229,"strokeWidth":286},"120","296","560","60",[211,316,318],{"x":213,"y":317,"textAnchor":215,"fontSize":290,"fontWeight":217,"fill":218},"322","@invariant runs after every rule",[211,320,322],{"x":213,"y":321,"textAnchor":215,"fontSize":295,"fill":218},"342","assert system under test matches model",[96,324],{"x1":213,"y1":325,"x2":213,"y2":326,"stroke":218,"strokeWidth":301,"strokeDashArray":327},"248","294",[303,303],[329,330,331],"figcaption",{},"Initialize seeds the model once; create rules push handles into a Bundle and mutate rules draw from it; the invariant compares the real system against the model after every step.",[33,333,335],{"id":334},"step-by-step-implementation","Step-by-step implementation",[10,337,338,339,342],{},"We will test a bounded least-recently-used cache. The reference model is an ",[47,340,341],{},"OrderedDict","; the system under test is the same here for illustration, but in practice it would be the real C-accelerated or Redis-backed implementation.",[344,345,347],"h3",{"id":346},"_1-subclass-rulebasedstatemachine-and-declare-a-bundle","1. Subclass RuleBasedStateMachine and declare a Bundle",[85,349,353],{"className":350,"code":351,"language":352,"meta":90,"style":90},"language-python shiki shiki-themes github-light github-dark","# test_lru_state.py\nfrom collections import OrderedDict\nfrom hypothesis import strategies as st\nfrom hypothesis.stateful import (\n    RuleBasedStateMachine, Bundle, rule, initialize, invariant, precondition,\n)\n\nCAPACITY = 4\n\nclass BoundedLRU:\n    \"\"\"System under test: a fixed-capacity LRU cache.\"\"\"\n    def __init__(self, capacity: int) -> None:\n        self.capacity = capacity\n        self._data: \"OrderedDict[int, str]\" = OrderedDict()\n\n    def put(self, key: int, value: str) -> None:\n        if key in self._data:\n            self._data.move_to_end(key)\n        self._data[key] = value\n        if len(self._data) > self.capacity:\n            self._data.popitem(last=False)  # evict least-recently-used\n\n    def get(self, key: int) -> str | None:\n        if key not in self._data:\n            return None\n        self._data.move_to_end(key)\n        return self._data[key]\n\nclass LRUStateMachine(RuleBasedStateMachine):\n    keys = Bundle(\"keys\")  # queue of keys we have actually inserted\n","python",[47,354,355,360,366,372,378,384,390,397,403,408,414,420,426,432,438,443,449,455,461,467,473,479,484,490,496,502,508,514,519,525],{"__ignoreMap":90},[94,356,357],{"class":96,"line":97},[94,358,359],{},"# test_lru_state.py\n",[94,361,363],{"class":96,"line":362},2,[94,364,365],{},"from collections import OrderedDict\n",[94,367,369],{"class":96,"line":368},3,[94,370,371],{},"from hypothesis import strategies as st\n",[94,373,375],{"class":96,"line":374},4,[94,376,377],{},"from hypothesis.stateful import (\n",[94,379,381],{"class":96,"line":380},5,[94,382,383],{},"    RuleBasedStateMachine, Bundle, rule, initialize, invariant, precondition,\n",[94,385,387],{"class":96,"line":386},6,[94,388,389],{},")\n",[94,391,393],{"class":96,"line":392},7,[94,394,396],{"emptyLinePlaceholder":395},true,"\n",[94,398,400],{"class":96,"line":399},8,[94,401,402],{},"CAPACITY = 4\n",[94,404,406],{"class":96,"line":405},9,[94,407,396],{"emptyLinePlaceholder":395},[94,409,411],{"class":96,"line":410},10,[94,412,413],{},"class BoundedLRU:\n",[94,415,417],{"class":96,"line":416},11,[94,418,419],{},"    \"\"\"System under test: a fixed-capacity LRU cache.\"\"\"\n",[94,421,423],{"class":96,"line":422},12,[94,424,425],{},"    def __init__(self, capacity: int) -> None:\n",[94,427,429],{"class":96,"line":428},13,[94,430,431],{},"        self.capacity = capacity\n",[94,433,435],{"class":96,"line":434},14,[94,436,437],{},"        self._data: \"OrderedDict[int, str]\" = OrderedDict()\n",[94,439,441],{"class":96,"line":440},15,[94,442,396],{"emptyLinePlaceholder":395},[94,444,446],{"class":96,"line":445},16,[94,447,448],{},"    def put(self, key: int, value: str) -> None:\n",[94,450,452],{"class":96,"line":451},17,[94,453,454],{},"        if key in self._data:\n",[94,456,458],{"class":96,"line":457},18,[94,459,460],{},"            self._data.move_to_end(key)\n",[94,462,464],{"class":96,"line":463},19,[94,465,466],{},"        self._data[key] = value\n",[94,468,470],{"class":96,"line":469},20,[94,471,472],{},"        if len(self._data) > self.capacity:\n",[94,474,476],{"class":96,"line":475},21,[94,477,478],{},"            self._data.popitem(last=False)  # evict least-recently-used\n",[94,480,482],{"class":96,"line":481},22,[94,483,396],{"emptyLinePlaceholder":395},[94,485,487],{"class":96,"line":486},23,[94,488,489],{},"    def get(self, key: int) -> str | None:\n",[94,491,493],{"class":96,"line":492},24,[94,494,495],{},"        if key not in self._data:\n",[94,497,499],{"class":96,"line":498},25,[94,500,501],{},"            return None\n",[94,503,505],{"class":96,"line":504},26,[94,506,507],{},"        self._data.move_to_end(key)\n",[94,509,511],{"class":96,"line":510},27,[94,512,513],{},"        return self._data[key]\n",[94,515,517],{"class":96,"line":516},28,[94,518,396],{"emptyLinePlaceholder":395},[94,520,522],{"class":96,"line":521},29,[94,523,524],{},"class LRUStateMachine(RuleBasedStateMachine):\n",[94,526,528],{"class":96,"line":527},30,[94,529,530],{},"    keys = Bundle(\"keys\")  # queue of keys we have actually inserted\n",[10,532,533,534,537],{},"The ",[47,535,536],{},"Bundle(\"keys\")"," is the spine of the test: it guarantees that rules which read or evict only ever reference keys some earlier rule inserted.",[344,539,541],{"id":540},"_2-seed-state-with-initialize","2. Seed state with @initialize",[85,543,545],{"className":350,"code":544,"language":352,"meta":90,"style":90},"    @initialize()\n    def setup(self) -> None:\n        # Runs once, before any rule. Construct system + model together.\n        self.cache = BoundedLRU(CAPACITY)\n        self.model: \"OrderedDict[int, str]\" = OrderedDict()\n",[47,546,547,552,557,562,567],{"__ignoreMap":90},[94,548,549],{"class":96,"line":97},[94,550,551],{},"    @initialize()\n",[94,553,554],{"class":96,"line":362},[94,555,556],{},"    def setup(self) -> None:\n",[94,558,559],{"class":96,"line":368},[94,560,561],{},"        # Runs once, before any rule. Construct system + model together.\n",[94,563,564],{"class":96,"line":374},[94,565,566],{},"        self.cache = BoundedLRU(CAPACITY)\n",[94,568,569],{"class":96,"line":380},[94,570,571],{},"        self.model: \"OrderedDict[int, str]\" = OrderedDict()\n",[10,573,574,576,577,580,581,583],{},[47,575,162],{}," is preferred over ",[47,578,579],{},"__init__"," for setup that should reset per sequence: Hypothesis instantiates the class once per example, and ",[47,582,162],{}," is sequenced into the generated program ahead of all rules.",[344,585,587],{"id":586},"_3-define-operations-as-rule-methods-pushing-into-the-bundle","3. Define operations as @rule methods, pushing into the Bundle",[85,589,591],{"className":350,"code":590,"language":352,"meta":90,"style":90},"    @rule(target=keys, key=st.integers(0, 1000), value=st.text(max_size=8))\n    def put(self, key: int, value: str):\n        self.cache.put(key, value)\n        # Mirror the operation in the model, including LRU eviction.\n        if key in self.model:\n            self.model.move_to_end(key)\n        self.model[key] = value\n        if len(self.model) > CAPACITY:\n            self.model.popitem(last=False)\n        return key  # pushed into the `keys` Bundle for later rules\n",[47,592,593,598,603,608,613,618,623,628,633,638],{"__ignoreMap":90},[94,594,595],{"class":96,"line":97},[94,596,597],{},"    @rule(target=keys, key=st.integers(0, 1000), value=st.text(max_size=8))\n",[94,599,600],{"class":96,"line":362},[94,601,602],{},"    def put(self, key: int, value: str):\n",[94,604,605],{"class":96,"line":368},[94,606,607],{},"        self.cache.put(key, value)\n",[94,609,610],{"class":96,"line":374},[94,611,612],{},"        # Mirror the operation in the model, including LRU eviction.\n",[94,614,615],{"class":96,"line":380},[94,616,617],{},"        if key in self.model:\n",[94,619,620],{"class":96,"line":386},[94,621,622],{},"            self.model.move_to_end(key)\n",[94,624,625],{"class":96,"line":392},[94,626,627],{},"        self.model[key] = value\n",[94,629,630],{"class":96,"line":399},[94,631,632],{},"        if len(self.model) > CAPACITY:\n",[94,634,635],{"class":96,"line":405},[94,636,637],{},"            self.model.popitem(last=False)\n",[94,639,640],{"class":96,"line":410},[94,641,642],{},"        return key  # pushed into the `keys` Bundle for later rules\n",[10,644,645,648,649,652],{},[47,646,647],{},"target=keys"," means the rule's return value is appended to the Bundle. A later rule names ",[47,650,651],{},"keys"," as an argument type to draw one of those values back out.",[344,654,656],{"id":655},"_4-guard-rules-with-precondition","4. Guard rules with @precondition",[85,658,660],{"className":350,"code":659,"language":352,"meta":90,"style":90},"    @precondition(lambda self: len(self.model) > 0)\n    @rule(key=keys)\n    def get_existing(self, key: int):\n        # `key` is drawn from the Bundle, so it was inserted earlier —\n        # but it may since have been evicted, which is the interesting case.\n        got = self.cache.get(key)\n        expected = self.model.get(key)\n        if expected is not None:\n            self.model.move_to_end(key)\n        assert got == expected\n",[47,661,662,667,672,677,682,687,692,697,702,706],{"__ignoreMap":90},[94,663,664],{"class":96,"line":97},[94,665,666],{},"    @precondition(lambda self: len(self.model) > 0)\n",[94,668,669],{"class":96,"line":362},[94,670,671],{},"    @rule(key=keys)\n",[94,673,674],{"class":96,"line":368},[94,675,676],{},"    def get_existing(self, key: int):\n",[94,678,679],{"class":96,"line":374},[94,680,681],{},"        # `key` is drawn from the Bundle, so it was inserted earlier —\n",[94,683,684],{"class":96,"line":380},[94,685,686],{},"        # but it may since have been evicted, which is the interesting case.\n",[94,688,689],{"class":96,"line":386},[94,690,691],{},"        got = self.cache.get(key)\n",[94,693,694],{"class":96,"line":392},[94,695,696],{},"        expected = self.model.get(key)\n",[94,698,699],{"class":96,"line":399},[94,700,701],{},"        if expected is not None:\n",[94,703,704],{"class":96,"line":405},[94,705,622],{},[94,707,708],{"class":96,"line":410},[94,709,710],{},"        assert got == expected\n",[10,712,713,715,716,719,720,723,724,726],{},[47,714,185],{}," disables ",[47,717,718],{},"get_existing"," until at least one key exists, so Hypothesis never wastes a step on an operation that cannot be meaningful. Stack the decorator ",[14,721,722],{},"above"," ",[47,725,154],{},".",[344,728,730],{"id":729},"_5-assert-state-with-invariant","5. Assert state with @invariant",[85,732,734],{"className":350,"code":733,"language":352,"meta":90,"style":90},"    @invariant()\n    def never_exceeds_capacity(self) -> None:\n        # Must hold in every reachable state, after every rule.\n        assert len(self.cache._data) \u003C= CAPACITY\n\n    @invariant()\n    def model_agreement(self) -> None:\n        assert list(self.cache._data.keys()) == list(self.model.keys())\n\n# pytest collects the TestCase automatically from the class:\nTestLRU = LRUStateMachine.TestCase\n",[47,735,736,741,746,751,756,760,764,769,774,778,783],{"__ignoreMap":90},[94,737,738],{"class":96,"line":97},[94,739,740],{},"    @invariant()\n",[94,742,743],{"class":96,"line":362},[94,744,745],{},"    def never_exceeds_capacity(self) -> None:\n",[94,747,748],{"class":96,"line":368},[94,749,750],{},"        # Must hold in every reachable state, after every rule.\n",[94,752,753],{"class":96,"line":374},[94,754,755],{},"        assert len(self.cache._data) \u003C= CAPACITY\n",[94,757,758],{"class":96,"line":380},[94,759,396],{"emptyLinePlaceholder":395},[94,761,762],{"class":96,"line":386},[94,763,740],{},[94,765,766],{"class":96,"line":392},[94,767,768],{},"    def model_agreement(self) -> None:\n",[94,770,771],{"class":96,"line":399},[94,772,773],{},"        assert list(self.cache._data.keys()) == list(self.model.keys())\n",[94,775,776],{"class":96,"line":405},[94,777,396],{"emptyLinePlaceholder":395},[94,779,780],{"class":96,"line":410},[94,781,782],{},"# pytest collects the TestCase automatically from the class:\n",[94,784,785],{"class":96,"line":416},[94,786,787],{},"TestLRU = LRUStateMachine.TestCase\n",[10,789,533,790,793,794,797],{},[47,791,792],{},".TestCase"," attribute is the pytest entry point. Assigning it to a module-level name lets ",[47,795,796],{},"pytest"," discover and run the machine like an ordinary test class.",[344,799,801],{"id":800},"_6-run-and-tune","6. Run and tune",[10,803,804,805,808],{},"Use ",[47,806,807],{},"settings"," to control sequence length and the number of distinct programs:",[85,810,812],{"className":350,"code":811,"language":352,"meta":90,"style":90},"from hypothesis import settings, HealthCheck\nfrom hypothesis.stateful import run_state_machine_as_test\n\ndef test_lru_thoroughly():\n    # run_state_machine_as_test runs the machine from inside a normal test,\n    # which is the supported place to attach settings or fixtures.\n    run_state_machine_as_test(\n        LRUStateMachine,\n        settings=settings(\n            max_examples=200,          # distinct operation sequences\n            stateful_step_count=60,    # max rules per sequence (default 50)\n            suppress_health_check=[HealthCheck.too_slow],\n        ),\n    )\n",[47,813,814,819,824,828,833,838,843,848,853,858,863,868,873,878],{"__ignoreMap":90},[94,815,816],{"class":96,"line":97},[94,817,818],{},"from hypothesis import settings, HealthCheck\n",[94,820,821],{"class":96,"line":362},[94,822,823],{},"from hypothesis.stateful import run_state_machine_as_test\n",[94,825,826],{"class":96,"line":368},[94,827,396],{"emptyLinePlaceholder":395},[94,829,830],{"class":96,"line":374},[94,831,832],{},"def test_lru_thoroughly():\n",[94,834,835],{"class":96,"line":380},[94,836,837],{},"    # run_state_machine_as_test runs the machine from inside a normal test,\n",[94,839,840],{"class":96,"line":386},[94,841,842],{},"    # which is the supported place to attach settings or fixtures.\n",[94,844,845],{"class":96,"line":392},[94,846,847],{},"    run_state_machine_as_test(\n",[94,849,850],{"class":96,"line":399},[94,851,852],{},"        LRUStateMachine,\n",[94,854,855],{"class":96,"line":405},[94,856,857],{},"        settings=settings(\n",[94,859,860],{"class":96,"line":410},[94,861,862],{},"            max_examples=200,          # distinct operation sequences\n",[94,864,865],{"class":96,"line":416},[94,866,867],{},"            stateful_step_count=60,    # max rules per sequence (default 50)\n",[94,869,870],{"class":96,"line":422},[94,871,872],{},"            suppress_health_check=[HealthCheck.too_slow],\n",[94,874,875],{"class":96,"line":428},[94,876,877],{},"        ),\n",[94,879,880],{"class":96,"line":434},[94,881,882],{},"    )\n",[10,884,885,887,888,891],{},[47,886,128],{}," caps the length of each generated program; ",[47,889,890],{},"max_examples"," controls how many programs are tried. Longer sequences find deeper bugs at linear cost.",[33,893,895],{"id":894},"verification","Verification",[10,897,898],{},"Confirm the machine actually exercises rules rather than skipping them. Run with statistics and verbosity:",[85,900,902],{"className":87,"code":901,"language":89,"meta":90,"style":90},"pytest test_lru_state.py --hypothesis-show-statistics -q\n",[47,903,904],{"__ignoreMap":90},[94,905,906,908,911,915],{"class":96,"line":97},[94,907,796],{"class":100},[94,909,910],{"class":104}," test_lru_state.py",[94,912,914],{"class":913},"sj4cs"," --hypothesis-show-statistics",[94,916,917],{"class":913}," -q\n",[10,919,920],{},"The statistics block reports how many examples ran and how often each rule fired. If a precondition is too strict you will see a rule reported as never selected. To watch the generated programs, set verbosity:",[85,922,924],{"className":350,"code":923,"language":352,"meta":90,"style":90},"from hypothesis import settings, Verbosity\nsettings(verbosity=Verbosity.debug)\n",[47,925,926,931],{"__ignoreMap":90},[94,927,928],{"class":96,"line":97},[94,929,930],{},"from hypothesis import settings, Verbosity\n",[94,932,933],{"class":96,"line":362},[94,934,935],{},"settings(verbosity=Verbosity.debug)\n",[10,937,938,939,942,943,54,946,949],{},"When a test fails, Hypothesis prints the ",[14,940,941],{},"minimal"," failing sequence as runnable code — ",[47,944,945],{},"state.put(key=0, value='')",[47,947,948],{},"state.get_existing(key=0)"," — which you can paste directly into a regression test. That shrunk reproduction is the payoff: a five-line program instead of a sixty-step random walk.",[33,951,953],{"id":952},"troubleshooting","Troubleshooting",[955,956,957,973],"table",{},[958,959,960],"thead",{},[961,962,963,967,970],"tr",{},[964,965,966],"th",{},"Symptom",[964,968,969],{},"Root cause",[964,971,972],{},"Fix",[974,975,976,1001,1020,1031,1055],"tbody",{},[961,977,978,985,994],{},[979,980,981,984],"td",{},[47,982,983],{},"Rule ... was never selected"," in statistics",[979,986,118,987,989,990,993],{},[47,988,185],{}," is never satisfied, or no Bundle ever has values for the rule's ",[47,991,992],{},"key=bundle"," argument",[979,995,996,997,1000],{},"Loosen the precondition, or ensure a producing rule (",[47,998,999],{},"target=bundle",") runs first",[961,1002,1003,1008,1011],{},[979,1004,1005],{},[47,1006,1007],{},"FailedHealthCheck: too_slow",[979,1009,1010],{},"Each rule does real I\u002FO and 50 steps blow the deadline",[979,1012,1013,1014,1017,1018],{},"Add ",[47,1015,1016],{},"suppress_health_check=[HealthCheck.too_slow]"," and\u002For lower ",[47,1019,128],{},[961,1021,1022,1025,1028],{},[979,1023,1024],{},"Invariant fails only at high step counts",[979,1026,1027],{},"A leak or off-by-one that needs accumulation to surface",[979,1029,1030],{},"Keep it failing; this is the intended catch. Read the shrunk sequence",[961,1032,1033,1039,1049],{},[979,1034,1035,1038],{},[47,1036,1037],{},"consumes(bundle)"," value reused unexpectedly",[979,1040,1041,1042,1045,1046,1048],{},"Used ",[47,1043,1044],{},"bundle"," (peek) where you needed ",[47,1047,1037],{}," (remove)",[979,1050,804,1051,1054],{},[47,1052,1053],{},"consumes()"," when a value must not be drawn again, e.g. after deletion",[961,1056,1057,1060,1068],{},[979,1058,1059],{},"Model and system disagree immediately",[979,1061,1062,1064,1065,1067],{},[47,1063,162],{}," did not reset both, or ",[47,1066,579],{}," state leaked across examples",[979,1069,1070,1071],{},"Move all per-sequence setup into ",[47,1072,162],{},[10,1074,1075,1076,1078,1079,1081,1082,1085,1086,1089],{},"A frequent precondition mistake worth its own note: stacking order matters. ",[47,1077,185],{}," must sit above ",[47,1080,154],{}," in the decorator stack, and the lambda receives ",[47,1083,1084],{},"self",", so it can inspect the model (",[47,1087,1088],{},"lambda self: self.model",") but not yet the drawn arguments.",[33,1091,1093],{"id":1092},"frequently-asked-questions","Frequently Asked Questions",[10,1095,1096,1105,1106,1108,1109,1111],{},[150,1097,1098,1099,1101,1102,1104],{},"What is the difference between ",[47,1100,154],{}," and ",[47,1103,162],{}," in a Hypothesis state machine?","\nAn ",[47,1107,162],{}," method runs at most once per generated sequence, before any rule, and is used to seed deterministic starting state. An ",[47,1110,154],{}," method is a candidate step that Hypothesis can call zero or more times in any order, subject to its preconditions, with arguments drawn from bound strategies.",[10,1113,1114,1117,1118,1120,1121,1124,1125,1128],{},[150,1115,1116],{},"How do Bundles pass created objects between rules?","\nA ",[47,1119,57],{}," is a named queue of values produced by earlier rules. A rule declares ",[47,1122,1123],{},"target=my_bundle"," to push its return value onto the queue, and another rule names ",[47,1126,1127],{},"my_bundle"," as an argument type to draw a previously created value, so Hypothesis only ever references objects that actually exist.",[10,1130,1131,1137,1138,1140],{},[150,1132,1133,1134,1136],{},"When does an ",[47,1135,177],{}," method run during stateful testing?","\nBy default an ",[47,1139,177],{}," runs after every rule execution, once the machine has been initialized. It asserts properties that must hold in every reachable state, independent of which rule sequence produced that state.",[10,1142,1143,1146,1147,1150,1151,1153,1154,1156],{},[150,1144,1145],{},"How do I control how many steps a stateful test executes?","\nApply ",[47,1148,1149],{},"settings(stateful_step_count=N)"," to the machine (alongside ",[47,1152,890],{}," for the number of distinct sequences). ",[47,1155,128],{}," caps the length of each generated rule program; the default is 50.",[10,1158,1159,1165,1166,1169,1170,1172],{},[150,1160,1161,1162,1164],{},"Can I run a ",[47,1163,53],{}," without pytest collecting it as a class?","\nYes. Call ",[47,1167,1168],{},"run_state_machine_as_test(MachineClass)"," inside a normal test function. This is the supported way to attach a ",[47,1171,807],{}," object or run the machine from a parametrized or fixture-driven context.",[33,1174,1176],{"id":1175},"related-guides","Related guides",[38,1178,1179,1185,1195,1202],{},[41,1180,1181,1182,1184],{},"Ground the generation primitives first with ",[22,1183,25],{"href":24},", then return here to sequence them into programs.",[41,1186,1187,1188,1192,1193,726],{},"Build the data your rules draw from using ",[22,1189,1191],{"href":1190},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Fgenerating-custom-strategies-with-hypothesisstrategies\u002F","custom strategies with hypothesis.strategies"," and the composition techniques in ",[22,1194,30],{"href":29},[41,1196,1197,1198,726],{},"Apply this model concretely to web services in ",[22,1199,1201],{"href":1200},"\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002Fmodeling-rest-apis-as-state-machines\u002F","Modeling REST APIs as State Machines",[41,1203,1204,1205,726],{},"When long sequences blow the clock, trim them with ",[22,1206,1208],{"href":1207},"\u002Fproperty-based-fuzz-testing-strategies\u002Fhypothesis-framework-fundamentals\u002Freducing-hypothesis-test-execution-time\u002F","reducing Hypothesis test execution time",[10,1210,1211,1212],{},"← Back to ",[22,1213,1215],{"href":1214},"\u002Fproperty-based-fuzz-testing-strategies\u002F","Property-Based & Fuzz Testing Strategies",[1217,1218,1219],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":90,"searchDepth":362,"depth":362,"links":1221},[1222,1223,1224,1232,1233,1234,1235],{"id":35,"depth":362,"text":36},{"id":114,"depth":362,"text":115},{"id":334,"depth":362,"text":335,"children":1225},[1226,1227,1228,1229,1230,1231],{"id":346,"depth":368,"text":347},{"id":540,"depth":368,"text":541},{"id":586,"depth":368,"text":587},{"id":655,"depth":368,"text":656},{"id":729,"depth":368,"text":730},{"id":800,"depth":368,"text":801},{"id":894,"depth":362,"text":895},{"id":952,"depth":362,"text":953},{"id":1092,"depth":362,"text":1093},{"id":1175,"depth":362,"text":1176},"Drive Hypothesis RuleBasedStateMachine with @rule, @initialize, Bundle, and @invariant to fuzz stateful Python systems and shrink failing operation sequences.","md",{"slug":1239,"type":1240,"breadcrumb":1241,"datePublished":1242,"dateModified":1242,"faq":1243,"howto":1257},"stateful-and-model-based-testing","cluster","Stateful Testing","2026-06-18",[1244,1247,1249,1252,1254],{"q":1245,"a":1246},"What is the difference between @rule and @initialize in a Hypothesis state machine?","An @initialize method runs at most once per test sequence, before any rule, and is used to seed deterministic starting state. An @rule method is a candidate step that Hypothesis can call zero or more times in any order subject to its preconditions.",{"q":1116,"a":1248},"A Bundle is a named queue of values produced by earlier rules. A rule declares targets=my_bundle to push its return value, and consumes my_bundle as an argument to draw a previously created value, so Hypothesis only references objects that actually exist.",{"q":1250,"a":1251},"When does an @invariant method run during stateful testing?","By default an @invariant runs after every rule execution, once the machine has been initialized. It asserts properties that must hold in every reachable state, independent of which rule sequence produced that state.",{"q":1145,"a":1253},"Wrap the machine class with settings(stateful_step_count=N) (and max_examples for the number of distinct sequences). stateful_step_count caps the length of each generated rule sequence; the default is 50.",{"q":1255,"a":1256},"Can I run a RuleBasedStateMachine without pytest collecting it as a class?","Yes. Call run_state_machine_as_test(MachineClass) inside a normal test function. This is the supported way to attach settings or run the machine from a parametrized or fixture-driven context.",{"name":1258,"description":1259,"steps":1260},"How to build a Hypothesis RuleBasedStateMachine","Model a stateful system as rules, bundles, and invariants so Hypothesis can fuzz operation sequences and shrink failures.",[1261,1264,1267,1270,1273,1276],{"name":1262,"text":1263},"Subclass RuleBasedStateMachine","Create a class inheriting from hypothesis.stateful.RuleBasedStateMachine and declare any Bundles it needs as class attributes.",{"name":1265,"text":1266},"Seed state with @initialize","Add an @initialize method that constructs the system under test and any reference model once per sequence.",{"name":1268,"text":1269},"Define operations as @rule methods","Express each operation as an @rule, drawing inputs from strategies and pushing created objects into Bundles via the target argument.",{"name":1271,"text":1272},"Guard rules with @precondition","Attach @precondition to rules that are only valid in certain states so Hypothesis never generates impossible transitions.",{"name":1274,"text":1275},"Assert state with @invariant","Add @invariant methods that compare the system against the model after every step.",{"name":1277,"text":1278},"Run and tune","Execute the machine via pytest or run_state_machine_as_test, then tune settings(stateful_step_count, max_examples) for coverage versus speed.","\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing",{"title":5,"description":1236},"property-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002Findex","sqSnfOPbH6BtuTafu2SMZvkKHJssPDMGLjdpJU1s8po",1781793487950]