Automating Edge Cases: Mastering Model-Based Testing with Hypothesis

ArjanCodes////4 min read

Overview

Software testing often feels like a game of whack-a-mole. You write a unit test for a specific scenario, but a bug slips through because of a specific sequence of actions you never considered. (or stateful testing) solves this by treating your application as a state machine. Instead of manual scripts, you define the possible actions and the rules that must always hold true. This technique allows a testing framework to explore thousands of execution paths, finding deep, logic-breaking bugs that standard unit tests frequently miss.

Prerequisites

To get the most out of this tutorial, you should have a solid grasp of and basic . Familiarity with is helpful since we will be using it as our test runner. You don't need prior experience with , though understanding the concept of generating random inputs will give you a head start.

Key Libraries & Tools

  • : A powerful library for property-based and stateful testing in .
  • : The standard testing framework used to execute our state machines.
  • HypoFuzz: An extension that provides fuzzing capabilities to find malformed input vulnerabilities.
  • Crosshair: A tool that analyzes code and type annotations to verify post-conditions.

Code Walkthrough

We start by defining a state machine class that inherits from RuleBasedStateMachine. We initialize our state—in this case, an Order and a list to track LineItem objects.

Automating Edge Cases: Mastering Model-Based Testing with Hypothesis
How to Use Hypothesis for Model-based Testing (Step by Step)
from hypothesis.stateful import RuleBasedStateMachine, rule, precondition
from hypothesis import strategies as st

class OrderTest(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.order = Order(customer="John Doe")
        self.created_items = []

Defining Rules and Actions

Rules are the actions can perform. We use the @rule decorator to tell the framework how to generate inputs. For example, creating a line item involves generating random text and integers.

@rule(description=st.text(), price=st.integers(), quantity=st.integers())
def create_line_item(self, description, price, quantity):
    item = LineItem(description, price, quantity)
    self.created_items.append(item)

Constraints with Preconditions

You cannot remove an item from an empty order. We use the @precondition decorator to ensure only attempts actions when they make logical sense.

@precondition(lambda self: len(self.order.line_items) > 0)
@rule(data=st.data())
def remove_line_item(self, data):
    item = data.draw(st.sampled_from(self.order.line_items))
    self.order.remove_line_item(item)

Verifying Invariants

Invariants (or sanity checks) are properties that must remain true after every single action. We check that the cached total in our Order class matches the actual sum of the items.

@rule()
def total_agrees(self):
    expected = sum(item.total for item in self.order.line_items)
    assert self.order.total == expected

Syntax Notes

  • st.data(): This strategy allows you to "draw" values inside the method body, which is vital when the input depends on the current state (like picking an item from an existing list).
  • st.sampled_from(): A strategy that picks an element from a specific collection, ensuring our test doesn't try to interact with objects that don't exist.
  • RuleBasedStateMachine: The core class from that manages the lifecycle of the stateful test.

Practical Examples

Model-based testing shines in systems with complex state transitions. Think of a banking API where users can deposit, withdraw, and transfer funds. A state machine can verify that no sequence of transfers results in a negative balance or "ghost" money. It is equally effective for testing file systems, database drivers, or UI workflows where the order of clicks matters.

Tips & Gotchas

  • Shrinking: When finds a bug, it will try to find the smallest possible sequence of actions to reproduce it. This makes debugging significantly easier.
  • Caching Traps: Be wary of manual cache invalidation. As shown in our walkthrough, updating a total manually often leads to bugs when the same object is added twice. Recomputing state or using robust observers is often safer.
  • Performance: Stateful tests run many more iterations than unit tests. Run them continuously in CI, but keep them focused on high-risk logic to avoid slowing down your development loop.
Topic DensityMention share of the most discussed topics · 17 mentions across 9 distinct topics
35%· products
18%· type
12%· products
6%· products
6%· products
Other topics
24%
End of Article
Source video
Automating Edge Cases: Mastering Model-Based Testing with Hypothesis

How to Use Hypothesis for Model-based Testing (Step by Step)

Watch

ArjanCodes // 22:35

On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
33.3%5
Python
20.0%3
Python
20.0%3
Pydantic
13.3%2
4 min read0%
4 min read