[{"data":1,"prerenderedAt":869},["ShallowReactive",2],{"page-\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002Fmodeling-rest-apis-as-state-machines\u002F":3},{"id":4,"title":5,"body":6,"description":832,"extension":833,"meta":834,"navigation":149,"path":865,"seo":866,"stem":867,"__hash__":868},"content\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002Fmodeling-rest-apis-as-state-machines\u002Findex.md","Modeling REST APIs as State Machines",{"type":7,"value":8,"toc":825},"minimark",[9,23,28,78,82,85,660,664,697,701,777,781,794,807,816,821],[10,11,12,13,17,18,22],"p",{},"You have a CRUD REST service and a pile of example tests that each exercise one endpoint, yet production still throws 404s on resources that were just created, or returns a deleted record from a list endpoint. The defect lives in the ",[14,15,16],"em",{},"interaction"," between requests — create, update, delete, re-read — not in any single handler. Hypothesis stateful testing generates those request sequences for you and shrinks any failure to a minimal reproduction. This guide maps REST verbs onto a ",[19,20,21],"code",{},"RuleBasedStateMachine",": endpoints become rules, created resources flow through Bundles, and business rules become invariants.",[24,25,27],"h2",{"id":26},"prerequisites","Prerequisites",[29,30,31,42,53],"ul",{},[32,33,34,35,38,39],"li",{},"Python 3.9+, ",[19,36,37],{},"hypothesis >= 6.0",", ",[19,40,41],{},"pytest >= 7.0",[32,43,44,45,48,49,52],{},"A web app exposing CRUD endpoints. The example uses FastAPI with ",[19,46,47],{},"starlette.testclient.TestClient"," (",[19,50,51],{},"pip install \"fastapi>=0.110\" httpx","), but the pattern applies to any client.",[32,54,55,56,61,62,38,65,38,68,38,71,38,74,77],{},"Familiarity with the primitives in ",[57,58,60],"a",{"href":59},"\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002F","Stateful and Model-Based Testing"," — ",[19,63,64],{},"@rule",[19,66,67],{},"@initialize",[19,69,70],{},"Bundle",[19,72,73],{},"@invariant",[19,75,76],{},"@precondition",".",[24,79,81],{"id":80},"solution","Solution",[10,83,84],{},"The mapping is mechanical once internalized: each verb is a transition, the set of live resource ids is a Bundle, and the server's contract is a set of invariants checked against an in-memory model.",[86,87,92],"pre",{"className":88,"code":89,"language":90,"meta":91,"style":91},"language-python shiki shiki-themes github-light github-dark","# test_items_api_state.py\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.testclient import TestClient\nfrom hypothesis import strategies as st, settings, HealthCheck\nfrom hypothesis.stateful import (\n    RuleBasedStateMachine, Bundle, rule, initialize, invariant,\n    precondition, consumes,\n)\n\n# --- A minimal system under test -------------------------------------------\napp = FastAPI()\n_DB: dict[int, dict] = {}\n_NEXT = {\"id\": 1}\n\n@app.post(\"\u002Fitems\")\ndef create_item(body: dict):\n    item_id = _NEXT[\"id\"]; _NEXT[\"id\"] += 1\n    _DB[item_id] = {\"id\": item_id, \"name\": body[\"name\"]}\n    return _DB[item_id]\n\n@app.get(\"\u002Fitems\u002F{item_id}\")\ndef read_item(item_id: int):\n    if item_id not in _DB:\n        raise HTTPException(status_code=404)\n    return _DB[item_id]\n\n@app.put(\"\u002Fitems\u002F{item_id}\")\ndef update_item(item_id: int, body: dict):\n    if item_id not in _DB:\n        raise HTTPException(status_code=404)\n    _DB[item_id][\"name\"] = body[\"name\"]\n    return _DB[item_id]\n\n@app.delete(\"\u002Fitems\u002F{item_id}\")\ndef delete_item(item_id: int):\n    if item_id not in _DB:\n        raise HTTPException(status_code=404)\n    del _DB[item_id]\n    return {\"deleted\": item_id}\n\n@app.get(\"\u002Fitems\")\ndef list_items():\n    return list(_DB.values())\n\n# --- The state machine -----------------------------------------------------\nnames = st.text(min_size=1, max_size=12)\n\nclass ItemsAPIMachine(RuleBasedStateMachine):\n    items = Bundle(\"items\")  # server-issued ids of live resources\n\n    @initialize()\n    def setup(self):\n        _DB.clear(); _NEXT[\"id\"] = 1     # reset server state per sequence\n        self.client = TestClient(app)\n        self.model: dict[int, str] = {}  # id -> expected name\n\n    @rule(target=items, name=names)\n    def create(self, name: str):\n        resp = self.client.post(\"\u002Fitems\", json={\"name\": name})\n        assert resp.status_code == 200\n        item_id = resp.json()[\"id\"]      # use the REAL server id\n        self.model[item_id] = name\n        return item_id                   # push id into the `items` Bundle\n\n    @rule(item_id=items, name=names)\n    def update(self, item_id: int, name: str):\n        resp = self.client.put(f\"\u002Fitems\u002F{item_id}\", json={\"name\": name})\n        assert resp.status_code == 200\n        self.model[item_id] = name\n\n    @rule(item_id=items)\n    def read(self, item_id: int):\n        resp = self.client.get(f\"\u002Fitems\u002F{item_id}\")\n        assert resp.status_code == 200\n        assert resp.json()[\"name\"] == self.model[item_id]\n\n    @rule(item_id=consumes(items))       # consumes => id never drawn again\n    def delete(self, item_id: int):\n        resp = self.client.delete(f\"\u002Fitems\u002F{item_id}\")\n        assert resp.status_code == 200\n        del self.model[item_id]\n        # Contract: a deleted resource must now 404.\n        assert self.client.get(f\"\u002Fitems\u002F{item_id}\").status_code == 404\n\n    @precondition(lambda self: True)\n    @invariant()\n    def list_count_matches_model(self):\n        # Business rule: the collection endpoint lists exactly the live items.\n        listed = self.client.get(\"\u002Fitems\").json()\n        assert len(listed) == len(self.model)\n        assert {i[\"id\"] for i in listed} == set(self.model)\n\nTestItemsAPI = ItemsAPIMachine.TestCase\nTestItemsAPI.settings = settings(\n    max_examples=100,\n    stateful_step_count=40,\n    suppress_health_check=[HealthCheck.too_slow],\n)\n","python","",[19,93,94,102,108,114,120,126,132,138,144,151,157,163,169,175,180,186,192,198,204,210,215,221,227,233,239,244,249,255,261,266,271,277,282,287,293,299,304,309,315,321,326,332,338,344,349,355,361,366,372,378,383,389,395,401,407,413,418,424,430,436,442,448,454,460,465,471,477,483,488,493,498,504,510,516,521,527,532,538,544,550,555,561,567,573,578,584,590,596,602,608,614,620,625,631,637,643,649,655],{"__ignoreMap":91},[95,96,99],"span",{"class":97,"line":98},"line",1,[95,100,101],{},"# test_items_api_state.py\n",[95,103,105],{"class":97,"line":104},2,[95,106,107],{},"from fastapi import FastAPI, HTTPException\n",[95,109,111],{"class":97,"line":110},3,[95,112,113],{},"from fastapi.testclient import TestClient\n",[95,115,117],{"class":97,"line":116},4,[95,118,119],{},"from hypothesis import strategies as st, settings, HealthCheck\n",[95,121,123],{"class":97,"line":122},5,[95,124,125],{},"from hypothesis.stateful import (\n",[95,127,129],{"class":97,"line":128},6,[95,130,131],{},"    RuleBasedStateMachine, Bundle, rule, initialize, invariant,\n",[95,133,135],{"class":97,"line":134},7,[95,136,137],{},"    precondition, consumes,\n",[95,139,141],{"class":97,"line":140},8,[95,142,143],{},")\n",[95,145,147],{"class":97,"line":146},9,[95,148,150],{"emptyLinePlaceholder":149},true,"\n",[95,152,154],{"class":97,"line":153},10,[95,155,156],{},"# --- A minimal system under test -------------------------------------------\n",[95,158,160],{"class":97,"line":159},11,[95,161,162],{},"app = FastAPI()\n",[95,164,166],{"class":97,"line":165},12,[95,167,168],{},"_DB: dict[int, dict] = {}\n",[95,170,172],{"class":97,"line":171},13,[95,173,174],{},"_NEXT = {\"id\": 1}\n",[95,176,178],{"class":97,"line":177},14,[95,179,150],{"emptyLinePlaceholder":149},[95,181,183],{"class":97,"line":182},15,[95,184,185],{},"@app.post(\"\u002Fitems\")\n",[95,187,189],{"class":97,"line":188},16,[95,190,191],{},"def create_item(body: dict):\n",[95,193,195],{"class":97,"line":194},17,[95,196,197],{},"    item_id = _NEXT[\"id\"]; _NEXT[\"id\"] += 1\n",[95,199,201],{"class":97,"line":200},18,[95,202,203],{},"    _DB[item_id] = {\"id\": item_id, \"name\": body[\"name\"]}\n",[95,205,207],{"class":97,"line":206},19,[95,208,209],{},"    return _DB[item_id]\n",[95,211,213],{"class":97,"line":212},20,[95,214,150],{"emptyLinePlaceholder":149},[95,216,218],{"class":97,"line":217},21,[95,219,220],{},"@app.get(\"\u002Fitems\u002F{item_id}\")\n",[95,222,224],{"class":97,"line":223},22,[95,225,226],{},"def read_item(item_id: int):\n",[95,228,230],{"class":97,"line":229},23,[95,231,232],{},"    if item_id not in _DB:\n",[95,234,236],{"class":97,"line":235},24,[95,237,238],{},"        raise HTTPException(status_code=404)\n",[95,240,242],{"class":97,"line":241},25,[95,243,209],{},[95,245,247],{"class":97,"line":246},26,[95,248,150],{"emptyLinePlaceholder":149},[95,250,252],{"class":97,"line":251},27,[95,253,254],{},"@app.put(\"\u002Fitems\u002F{item_id}\")\n",[95,256,258],{"class":97,"line":257},28,[95,259,260],{},"def update_item(item_id: int, body: dict):\n",[95,262,264],{"class":97,"line":263},29,[95,265,232],{},[95,267,269],{"class":97,"line":268},30,[95,270,238],{},[95,272,274],{"class":97,"line":273},31,[95,275,276],{},"    _DB[item_id][\"name\"] = body[\"name\"]\n",[95,278,280],{"class":97,"line":279},32,[95,281,209],{},[95,283,285],{"class":97,"line":284},33,[95,286,150],{"emptyLinePlaceholder":149},[95,288,290],{"class":97,"line":289},34,[95,291,292],{},"@app.delete(\"\u002Fitems\u002F{item_id}\")\n",[95,294,296],{"class":97,"line":295},35,[95,297,298],{},"def delete_item(item_id: int):\n",[95,300,302],{"class":97,"line":301},36,[95,303,232],{},[95,305,307],{"class":97,"line":306},37,[95,308,238],{},[95,310,312],{"class":97,"line":311},38,[95,313,314],{},"    del _DB[item_id]\n",[95,316,318],{"class":97,"line":317},39,[95,319,320],{},"    return {\"deleted\": item_id}\n",[95,322,324],{"class":97,"line":323},40,[95,325,150],{"emptyLinePlaceholder":149},[95,327,329],{"class":97,"line":328},41,[95,330,331],{},"@app.get(\"\u002Fitems\")\n",[95,333,335],{"class":97,"line":334},42,[95,336,337],{},"def list_items():\n",[95,339,341],{"class":97,"line":340},43,[95,342,343],{},"    return list(_DB.values())\n",[95,345,347],{"class":97,"line":346},44,[95,348,150],{"emptyLinePlaceholder":149},[95,350,352],{"class":97,"line":351},45,[95,353,354],{},"# --- The state machine -----------------------------------------------------\n",[95,356,358],{"class":97,"line":357},46,[95,359,360],{},"names = st.text(min_size=1, max_size=12)\n",[95,362,364],{"class":97,"line":363},47,[95,365,150],{"emptyLinePlaceholder":149},[95,367,369],{"class":97,"line":368},48,[95,370,371],{},"class ItemsAPIMachine(RuleBasedStateMachine):\n",[95,373,375],{"class":97,"line":374},49,[95,376,377],{},"    items = Bundle(\"items\")  # server-issued ids of live resources\n",[95,379,381],{"class":97,"line":380},50,[95,382,150],{"emptyLinePlaceholder":149},[95,384,386],{"class":97,"line":385},51,[95,387,388],{},"    @initialize()\n",[95,390,392],{"class":97,"line":391},52,[95,393,394],{},"    def setup(self):\n",[95,396,398],{"class":97,"line":397},53,[95,399,400],{},"        _DB.clear(); _NEXT[\"id\"] = 1     # reset server state per sequence\n",[95,402,404],{"class":97,"line":403},54,[95,405,406],{},"        self.client = TestClient(app)\n",[95,408,410],{"class":97,"line":409},55,[95,411,412],{},"        self.model: dict[int, str] = {}  # id -> expected name\n",[95,414,416],{"class":97,"line":415},56,[95,417,150],{"emptyLinePlaceholder":149},[95,419,421],{"class":97,"line":420},57,[95,422,423],{},"    @rule(target=items, name=names)\n",[95,425,427],{"class":97,"line":426},58,[95,428,429],{},"    def create(self, name: str):\n",[95,431,433],{"class":97,"line":432},59,[95,434,435],{},"        resp = self.client.post(\"\u002Fitems\", json={\"name\": name})\n",[95,437,439],{"class":97,"line":438},60,[95,440,441],{},"        assert resp.status_code == 200\n",[95,443,445],{"class":97,"line":444},61,[95,446,447],{},"        item_id = resp.json()[\"id\"]      # use the REAL server id\n",[95,449,451],{"class":97,"line":450},62,[95,452,453],{},"        self.model[item_id] = name\n",[95,455,457],{"class":97,"line":456},63,[95,458,459],{},"        return item_id                   # push id into the `items` Bundle\n",[95,461,463],{"class":97,"line":462},64,[95,464,150],{"emptyLinePlaceholder":149},[95,466,468],{"class":97,"line":467},65,[95,469,470],{},"    @rule(item_id=items, name=names)\n",[95,472,474],{"class":97,"line":473},66,[95,475,476],{},"    def update(self, item_id: int, name: str):\n",[95,478,480],{"class":97,"line":479},67,[95,481,482],{},"        resp = self.client.put(f\"\u002Fitems\u002F{item_id}\", json={\"name\": name})\n",[95,484,486],{"class":97,"line":485},68,[95,487,441],{},[95,489,491],{"class":97,"line":490},69,[95,492,453],{},[95,494,496],{"class":97,"line":495},70,[95,497,150],{"emptyLinePlaceholder":149},[95,499,501],{"class":97,"line":500},71,[95,502,503],{},"    @rule(item_id=items)\n",[95,505,507],{"class":97,"line":506},72,[95,508,509],{},"    def read(self, item_id: int):\n",[95,511,513],{"class":97,"line":512},73,[95,514,515],{},"        resp = self.client.get(f\"\u002Fitems\u002F{item_id}\")\n",[95,517,519],{"class":97,"line":518},74,[95,520,441],{},[95,522,524],{"class":97,"line":523},75,[95,525,526],{},"        assert resp.json()[\"name\"] == self.model[item_id]\n",[95,528,530],{"class":97,"line":529},76,[95,531,150],{"emptyLinePlaceholder":149},[95,533,535],{"class":97,"line":534},77,[95,536,537],{},"    @rule(item_id=consumes(items))       # consumes => id never drawn again\n",[95,539,541],{"class":97,"line":540},78,[95,542,543],{},"    def delete(self, item_id: int):\n",[95,545,547],{"class":97,"line":546},79,[95,548,549],{},"        resp = self.client.delete(f\"\u002Fitems\u002F{item_id}\")\n",[95,551,553],{"class":97,"line":552},80,[95,554,441],{},[95,556,558],{"class":97,"line":557},81,[95,559,560],{},"        del self.model[item_id]\n",[95,562,564],{"class":97,"line":563},82,[95,565,566],{},"        # Contract: a deleted resource must now 404.\n",[95,568,570],{"class":97,"line":569},83,[95,571,572],{},"        assert self.client.get(f\"\u002Fitems\u002F{item_id}\").status_code == 404\n",[95,574,576],{"class":97,"line":575},84,[95,577,150],{"emptyLinePlaceholder":149},[95,579,581],{"class":97,"line":580},85,[95,582,583],{},"    @precondition(lambda self: True)\n",[95,585,587],{"class":97,"line":586},86,[95,588,589],{},"    @invariant()\n",[95,591,593],{"class":97,"line":592},87,[95,594,595],{},"    def list_count_matches_model(self):\n",[95,597,599],{"class":97,"line":598},88,[95,600,601],{},"        # Business rule: the collection endpoint lists exactly the live items.\n",[95,603,605],{"class":97,"line":604},89,[95,606,607],{},"        listed = self.client.get(\"\u002Fitems\").json()\n",[95,609,611],{"class":97,"line":610},90,[95,612,613],{},"        assert len(listed) == len(self.model)\n",[95,615,617],{"class":97,"line":616},91,[95,618,619],{},"        assert {i[\"id\"] for i in listed} == set(self.model)\n",[95,621,623],{"class":97,"line":622},92,[95,624,150],{"emptyLinePlaceholder":149},[95,626,628],{"class":97,"line":627},93,[95,629,630],{},"TestItemsAPI = ItemsAPIMachine.TestCase\n",[95,632,634],{"class":97,"line":633},94,[95,635,636],{},"TestItemsAPI.settings = settings(\n",[95,638,640],{"class":97,"line":639},95,[95,641,642],{},"    max_examples=100,\n",[95,644,646],{"class":97,"line":645},96,[95,647,648],{},"    stateful_step_count=40,\n",[95,650,652],{"class":97,"line":651},97,[95,653,654],{},"    suppress_health_check=[HealthCheck.too_slow],\n",[95,656,658],{"class":97,"line":657},98,[95,659,143],{},[24,661,663],{"id":662},"why-this-works","Why this works",[10,665,666,667,670,671,674,675,678,679,682,683,686,687,689,690,693,694,696],{},"The Bundle is what makes generated request sequences ",[14,668,669],{},"realistic",": a ",[19,672,673],{},"read"," or ",[19,676,677],{},"delete"," only ever fires against an id the server actually issued from a prior ",[19,680,681],{},"create",", so Hypothesis never wastes steps probing random 404s and instead explores the meaningful state space. Using ",[19,684,685],{},"consumes(items)"," on ",[19,688,677],{}," removes the id from the queue, so the deleted-then-read contract is enforced by construction rather than by chance. The in-memory ",[19,691,692],{},"model"," dict is the oracle — cheap, obviously-correct code that the real handlers must agree with after every transition, which the ",[19,695,73],{}," checks.",[24,698,700],{"id":699},"edge-cases-and-failure-modes","Edge cases and failure modes",[29,702,703,717,740,749,771],{},[32,704,705,709,710,713,714,716],{},[706,707,708],"strong",{},"Shared server state leaks across sequences."," Module-level ",[19,711,712],{},"_DB"," must be reset in ",[19,715,67],{},"; otherwise resources from a previous example pollute the next and invariants fail spuriously. Prefer a fresh app\u002Fdatabase fixture per machine instance for real services.",[32,718,719,725,726,729,730,732,733,735,736,739],{},[706,720,721,724],{},[19,722,723],{},"consumes"," vs plain Bundle reference."," Using the bare Bundle (",[19,727,728],{},"item_id=items",") in ",[19,731,677],{}," would leave the deleted id drawable, and a later ",[19,734,673],{}," would assert 200 against a 404 — a false failure. Always ",[19,737,738],{},"consumes()"," on terminal operations.",[32,741,742,745,746,77],{},[706,743,744],{},"Non-deterministic ids."," If the server issues UUIDs or relies on wall-clock ordering, capture the id from the response body (as above) rather than predicting it; never hardcode ",[19,747,748],{},"item_id=1",[32,750,751,754,755,758,759,762,763,766,767,77],{},[706,752,753],{},"Slow real I\u002FO."," Hitting a live database for 40 steps across 100 examples is thousands of requests. Suppress ",[19,756,757],{},"HealthCheck.too_slow",", lower ",[19,760,761],{},"stateful_step_count",", or use an in-process ",[19,764,765],{},"TestClient"," as shown. See ",[57,768,770],{"href":769},"\u002Fproperty-based-fuzz-testing-strategies\u002Fhypothesis-framework-fundamentals\u002Freducing-hypothesis-test-execution-time\u002F","reducing Hypothesis test execution time",[32,772,773,776],{},[706,774,775],{},"Auth and pagination."," Endpoints requiring tokens or returning paged lists need the invariant to follow pagination; comparing only the first page against a growing model will fail past the page size.",[24,778,780],{"id":779},"frequently-asked-questions","Frequently Asked Questions",[10,782,783,786,787,789,790,793],{},[706,784,785],{},"How do I represent a POST that creates a resource in a Hypothesis state machine?","\nModel the POST as an ",[19,788,64],{}," whose ",[19,791,792],{},"target"," is a Bundle, returning the created resource id from the response. Hypothesis pushes that id into the Bundle so later GET, PUT, and DELETE rules draw a real, server-issued id rather than fabricating one.",[10,795,796,799,800,803,804,806],{},[706,797,798],{},"How do I model DELETE so the same id is not reused after removal?","\nConsume the id from the Bundle with ",[19,801,802],{},"consumes(resources)"," in the DELETE rule. ",[19,805,723],{}," removes the value from the queue, so no subsequent rule will draw a deleted id, matching the server's contract that a deleted resource returns 404.",[10,808,809,812,813,815],{},[706,810,811],{},"Where do business rules go in a REST state machine?","\nEncode them as ",[19,814,73],{}," methods that query the live API and compare against the in-memory model — for example asserting the list endpoint returns exactly the set of created-and-not-deleted ids after every step.",[10,817,818,819],{},"← Back to ",[57,820,60],{"href":59},[822,823,824],"style",{},"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);}",{"title":91,"searchDepth":104,"depth":104,"links":826},[827,828,829,830,831],{"id":26,"depth":104,"text":27},{"id":80,"depth":104,"text":81},{"id":662,"depth":104,"text":663},{"id":699,"depth":104,"text":700},{"id":779,"depth":104,"text":780},"Map REST endpoints to Hypothesis @rule methods, track created resources in Bundles, and assert business rules as invariants to fuzz CRUD APIs end to end.","md",{"slug":835,"type":836,"breadcrumb":837,"datePublished":838,"dateModified":838,"faq":839,"howto":846},"modeling-rest-apis-as-state-machines","long_tail","REST API State Machines","2026-06-18",[840,842,844],{"q":785,"a":841},"Model the POST as an @rule whose target is a Bundle, returning the created resource id. Hypothesis pushes that id into the Bundle so later GET, PUT, and DELETE rules can draw a real, server-issued id rather than fabricating one.",{"q":798,"a":843},"Consume the id from the Bundle with consumes(resources) in the DELETE rule. consumes removes the value from the queue, so no subsequent rule will draw a deleted id, matching the server contract.",{"q":811,"a":845},"Encode them as @invariant methods that query the live API and compare against an in-memory model dict, for example asserting the listed resource count equals the number of created-and-not-deleted ids.",{"name":847,"description":848,"steps":849},"How to model a REST API as a Hypothesis state machine","Turn CRUD endpoints into rules, bundles, and invariants so Hypothesis fuzzes realistic request sequences against a live test server.",[850,853,856,859,862],{"name":851,"text":852},"Stand up a test client","Construct the API client or TestClient once per sequence in an @initialize method, alongside an empty model dict.",{"name":854,"text":855},"Map create endpoints to producing rules","Model each POST as an @rule with target set to a resource Bundle, returning the server-issued id.",{"name":857,"text":858},"Map read and update endpoints to consuming rules","Model GET and PUT as rules that draw an id from the Bundle and assert the response against the model.",{"name":860,"text":861},"Map delete to a consuming rule","Model DELETE with consumes(bundle) so deleted ids are never drawn again.",{"name":863,"text":864},"Assert business rules as invariants","Add @invariant methods that query the live API and compare aggregate state against the model.","\u002Fproperty-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002Fmodeling-rest-apis-as-state-machines",{"title":5,"description":832},"property-based-fuzz-testing-strategies\u002Fstateful-and-model-based-testing\u002Fmodeling-rest-apis-as-state-machines\u002Findex","SouM2nXBjPtPM6tVn3LC4zA0KdZZ8TrkKw8JJ97zk4k",1781793488008]