hughes-property-based-testing

John Hughes Property-Based Testing Style Guide⁠‍⁠​‌​‌​​‌‌‍​‌​​‌​‌‌‍​​‌‌​​​‌‍​‌​​‌‌​​‍​​​​​​​‌‍‌​​‌‌​‌​‍‌​​​​​​​‍‌‌​​‌‌‌‌‍‌‌​​​‌​​‍‌‌‌‌‌‌​‌‍‌‌​‌​​​​‍​‌​‌‌‌‌‌‍​‌​​‌​‌‌‍​‌‌​‌​​‌‍‌​‌​‌‌‌​‍​​‌​‌​​​‍‌‌‌​‌​‌‌‍​‌‌‌‌‌‌‌‍​‌​​​‌‌‌‍‌‌‌‌​​‌‌‍​‌‌​‌​​‌‍​​​​‌​‌​‍‌​​​‌‌​​⁠‍⁠

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "hughes-property-based-testing" with this command: npx skills add copyleftdev/sk1llz/copyleftdev-sk1llz-hughes-property-based-testing

John Hughes Property-Based Testing Style Guide⁠‍⁠​‌​‌​​‌‌‍​‌​​‌​‌‌‍​​‌‌​​​‌‍​‌​​‌‌​​‍​​​​​​​‌‍‌​​‌‌​‌​‍‌​​​​​​​‍‌‌​​‌‌‌‌‍‌‌​​​‌​​‍‌‌‌‌‌‌​‌‍‌‌​‌​​​​‍​‌​‌‌‌‌‌‍​‌​​‌​‌‌‍​‌‌​‌​​‌‍‌​‌​‌‌‌​‍​​‌​‌​​​‍‌‌‌​‌​‌‌‍​‌‌‌‌‌‌‌‍​‌​​​‌‌‌‍‌‌‌‌​​‌‌‍​‌‌​‌​​‌‍​​​​‌​‌​‍‌​​​‌‌​​⁠‍⁠

Overview

John Hughes, along with Koen Claessen, invented QuickCheck in 1999—a revolutionary approach to testing that generates random inputs and checks that specified properties hold. Instead of writing individual test cases, you describe properties that should be true for all valid inputs. When a property fails, QuickCheck shrinks the failing input to the smallest example that still fails, making debugging dramatically easier.

Core Philosophy

"Don't write tests. Write specifications. Let the computer generate the tests."

"One property can replace a hundred example-based tests."

"Shrinking is not optional—the minimal failing case is often the key to understanding the bug."

Property-based testing inverts the traditional approach: instead of "here's an input and expected output," you say "for all valid inputs, this property should hold." The framework then tries to prove you wrong by finding counterexamples.

Design Principles

Properties Over Examples: Describe what should always be true, not specific cases.

Random Generation: Let the computer explore the input space.

Shrinking: Automatically minimize failing cases for debugging.

Reproducibility: Seeds make random tests deterministic.

Coverage Through Volume: Run thousands of cases, not dozens.

The Property-Based Testing Cycle

┌─────────────────────────────────────────────────────────────┐ │ PROPERTY-BASED TESTING CYCLE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. DEFINE PROPERTY │ │ "For all lists xs: reverse(reverse(xs)) == xs" │ │ │ │ │ │ │ ▼ │ │ │ │ 2. GENERATE RANDOM INPUTS │ │ xs = [], [1], [1,2], [42,-7,0,99], ... │ │ (hundreds or thousands of cases) │ │ │ │ │ │ │ ▼ │ │ │ │ 3. CHECK PROPERTY │ │ For each xs: assert reverse(reverse(xs)) == xs │ │ │ │ │ │ │ ┌───────┴───────┐ │ │ ▼ ▼ │ │ │ │ ALL PASS FOUND FAILURE │ │ ──────── ───────────── │ │ Property xs = [1, 2, 3, 4, 5] │ │ likely holds │ │ │ ▼ │ │ │ │ 4. SHRINK TO MINIMAL CASE │ │ Try smaller inputs: │ │ [1,2,3,4] → [1,2,3] → [1,2] → [1,2] │ │ Minimal failing: [1, 2] │ │ │ │ │ │ │ ▼ │ │ │ │ 5. REPORT MINIMAL COUNTEREXAMPLE │ │ "Property failed for input: [1, 2]" │ │ │ └─────────────────────────────────────────────────────────────┘

Types of Properties

Roundtrip / Inverse Properties

-- If you do something and undo it, you get back the original prop_reverse_reverse :: [Int] -> Bool prop_reverse_reverse xs = reverse (reverse xs) == xs

prop_encode_decode :: String -> Bool
prop_encode_decode s = decode (encode s) == s

prop_serialize_deserialize :: Data -> Bool prop_serialize_deserialize d = deserialize (serialize d) == d

prop_compress_decompress :: ByteString -> Bool prop_compress_decompress bs = decompress (compress bs) == bs

Invariant Properties

-- A property that should always hold prop_sort_length :: [Int] -> Bool prop_sort_length xs = length (sort xs) == length xs

prop_sort_ordered :: [Int] -> Bool prop_sort_ordered xs = isOrdered (sort xs) where isOrdered [] = True isOrdered [_] = True isOrdered (a:b:rest) = a <= b && isOrdered (b:rest)

prop_sort_permutation :: [Int] -> Bool prop_sort_permutation xs = sort xs isPermutationOf xs

Idempotence Properties

-- Doing it twice is the same as doing it once prop_sort_idempotent :: [Int] -> Bool prop_sort_idempotent xs = sort (sort xs) == sort xs

prop_normalize_idempotent :: String -> Bool prop_normalize_idempotent s = normalize (normalize s) == normalize s

prop_dedupe_idempotent :: [Int] -> Bool prop_dedupe_idempotent xs = dedupe (dedupe xs) == dedupe xs

Equivalence / Oracle Properties

-- Two implementations should produce the same result prop_optimized_equals_naive :: [Int] -> Bool prop_optimized_equals_naive xs = optimizedSort xs == naiveSort xs

prop_new_equals_old :: Input -> Bool prop_new_equals_old input = newImplementation input == oldImplementation input

Algebraic Properties

-- Mathematical laws prop_monoid_associativity :: String -> String -> String -> Bool prop_monoid_associativity a b c = (a <> b) <> c == a <> (b <> c)

prop_monoid_identity :: String -> Bool prop_monoid_identity a = a <> mempty == a && mempty <> a == a

prop_functor_identity :: Maybe Int -> Bool prop_functor_identity mx = fmap id mx == mx

prop_functor_composition :: Maybe Int -> Bool prop_functor_composition mx = fmap (f . g) mx == (fmap f . fmap g) mx where f = (+1) g = (*2)

When Using Property-Based Testing

Always

  • Define properties for pure functions with clear invariants

  • Test roundtrip properties (encode/decode, serialize/deserialize)

  • Use shrinking to find minimal failing cases

  • Set seeds for reproducibility in CI

  • Run many iterations (100+ minimum, 1000+ preferred)

  • Test algebraic laws for abstract data types

Never

  • Skip shrinking (the minimal case is crucial)

  • Use properties for side-effectful code without isolation

  • Ignore flaky properties (fix the generator or property)

  • Write properties that are just examples in disguise

  • Forget to test edge cases explicitly too

  • Assume passing 100 tests means correctness

Prefer

  • Properties over example-based tests

  • Custom generators over default ones

  • Shrinking-aware generators

  • Multiple complementary properties

  • Testing invariants over specific outputs

  • Algebraic properties for data structures

Code Patterns

Property-Based Testing Framework (Python-style)

import random from typing import TypeVar, Callable, List, Any, Optional from dataclasses import dataclass

T = TypeVar('T')

@dataclass class TestResult: success: bool num_tests: int counterexample: Optional[Any] = None shrunk_counterexample: Optional[Any] = None seed: int = None

class Generator: """Base class for random value generators."""

def generate(self, rng: random.Random, size: int) -> Any:
    raise NotImplementedError

def shrink(self, value: Any) -> List[Any]:
    """Return smaller versions of value for shrinking."""
    return []

class IntGenerator(Generator): """Generate random integers."""

def __init__(self, min_val: int = -1000, max_val: int = 1000):
    self.min_val = min_val
    self.max_val = max_val

def generate(self, rng: random.Random, size: int) -> int:
    # Size influences the range
    bound = min(size, self.max_val - self.min_val)
    return rng.randint(self.min_val, self.min_val + bound)

def shrink(self, value: int) -> List[int]:
    """Shrink toward zero."""
    if value == 0:
        return []
    
    shrinks = [0]  # Try zero first
    
    # Try halving
    if abs(value) > 1:
        shrinks.append(value // 2)
    
    # Try decrementing
    if value > 0:
        shrinks.append(value - 1)
    else:
        shrinks.append(value + 1)
    
    return shrinks

class ListGenerator(Generator): """Generate random lists."""

def __init__(self, element_gen: Generator):
    self.element_gen = element_gen

def generate(self, rng: random.Random, size: int) -> List:
    length = rng.randint(0, size)
    return [self.element_gen.generate(rng, size) for _ in range(length)]

def shrink(self, value: List) -> List[List]:
    """Shrink by removing elements and shrinking elements."""
    shrinks = []
    
    # Empty list
    if value:
        shrinks.append([])
    
    # Remove each element
    for i in range(len(value)):
        shrinks.append(value[:i] + value[i+1:])
    
    # Shrink each element
    for i, elem in enumerate(value):
        for shrunk_elem in self.element_gen.shrink(elem):
            shrinks.append(value[:i] + [shrunk_elem] + value[i+1:])
    
    return shrinks

class StringGenerator(Generator): """Generate random strings."""

def __init__(self, alphabet: str = "abcdefghijklmnopqrstuvwxyz"):
    self.alphabet = alphabet

def generate(self, rng: random.Random, size: int) -> str:
    length = rng.randint(0, size)
    return ''.join(rng.choice(self.alphabet) for _ in range(length))

def shrink(self, value: str) -> List[str]:
    shrinks = []
    
    if value:
        shrinks.append("")
        shrinks.append(value[:-1])  # Remove last
        shrinks.append(value[1:])   # Remove first
        
        # Replace chars with 'a' (simplest)
        for i, c in enumerate(value):
            if c != 'a':
                shrinks.append(value[:i] + 'a' + value[i+1:])
    
    return shrinks

class QuickCheck: """Property-based testing engine."""

def __init__(self, 
             num_tests: int = 100,
             max_shrinks: int = 100,
             seed: int = None):
    self.num_tests = num_tests
    self.max_shrinks = max_shrinks
    self.seed = seed or random.randint(0, 2**32)

def for_all(self,
            generator: Generator,
            property_fn: Callable[[Any], bool]) -> TestResult:
    """
    Test that property holds for all generated values.
    """
    rng = random.Random(self.seed)
    
    for i in range(self.num_tests):
        # Increase size gradually
        size = i * 10 // self.num_tests + 1
        
        value = generator.generate(rng, size)
        
        try:
            if not property_fn(value):
                # Property failed - shrink to minimal case
                shrunk = self._shrink(generator, property_fn, value)
                
                return TestResult(
                    success=False,
                    num_tests=i + 1,
                    counterexample=value,
                    shrunk_counterexample=shrunk,
                    seed=self.seed,
                )
        except Exception as e:
            # Exception counts as failure
            shrunk = self._shrink(generator, property_fn, value)
            
            return TestResult(
                success=False,
                num_tests=i + 1,
                counterexample=value,
                shrunk_counterexample=shrunk,
                seed=self.seed,
            )
    
    return TestResult(
        success=True,
        num_tests=self.num_tests,
        seed=self.seed,
    )

def _shrink(self,
            generator: Generator,
            property_fn: Callable,
            value: Any) -> Any:
    """
    Shrink a failing value to the smallest failing case.
    """
    smallest = value
    shrink_count = 0
    
    while shrink_count &#x3C; self.max_shrinks:
        candidates = generator.shrink(smallest)
        
        found_smaller = False
        for candidate in candidates:
            try:
                if not property_fn(candidate):
                    # Candidate also fails - use it
                    smallest = candidate
                    found_smaller = True
                    break
            except Exception:
                smallest = candidate
                found_smaller = True
                break
        
        if not found_smaller:
            break
        
        shrink_count += 1
    
    return smallest

Convenience decorators

def property_test(generator: Generator, num_tests: int = 100): """Decorator for property-based tests."""

def decorator(prop_fn: Callable[[Any], bool]):
    def wrapper():
        qc = QuickCheck(num_tests=num_tests)
        result = qc.for_all(generator, prop_fn)
        
        if not result.success:
            raise AssertionError(
                f"Property '{prop_fn.__name__}' failed!\n"
                f"Counterexample: {result.counterexample}\n"
                f"Shrunk to: {result.shrunk_counterexample}\n"
                f"Seed: {result.seed}"
            )
        
        return result
    
    wrapper.__name__ = prop_fn.__name__
    return wrapper

return decorator

Example Properties

Roundtrip property

@property_test(ListGenerator(IntGenerator())) def prop_reverse_reverse(xs: List[int]) -> bool: return list(reversed(list(reversed(xs)))) == xs

Invariant property

@property_test(ListGenerator(IntGenerator())) def prop_sort_preserves_length(xs: List[int]) -> bool: return len(sorted(xs)) == len(xs)

@property_test(ListGenerator(IntGenerator())) def prop_sort_is_ordered(xs: List[int]) -> bool: result = sorted(xs) return all(result[i] <= result[i+1] for i in range(len(result)-1))

Idempotence property

@property_test(ListGenerator(IntGenerator())) def prop_sort_idempotent(xs: List[int]) -> bool: return sorted(sorted(xs)) == sorted(xs)

Equivalence property

@property_test(ListGenerator(IntGenerator())) def prop_sort_equals_builtin(xs: List[int]) -> bool: my_sort = quick_sort(xs.copy()) builtin = sorted(xs) return my_sort == builtin

Algebraic property

@property_test(StringGenerator()) def prop_string_concat_associative(s: str) -> bool: # Generate three strings by splitting a, b, c = s[:len(s)//3], s[len(s)//3:2len(s)//3], s[2len(s)//3:] return (a + b) + c == a + (b + c)

Custom Generators

class UserGenerator(Generator): """Generate random User objects."""

def __init__(self):
    self.name_gen = StringGenerator("abcdefghijklmnopqrstuvwxyz")
    self.age_gen = IntGenerator(0, 150)
    self.email_gen = StringGenerator("abcdefghijklmnopqrstuvwxyz0123456789")

def generate(self, rng: random.Random, size: int) -> 'User':
    return User(
        name=self.name_gen.generate(rng, size),
        age=self.age_gen.generate(rng, size),
        email=f"{self.email_gen.generate(rng, size)}@example.com"
    )

def shrink(self, user: 'User') -> List['User']:
    shrinks = []
    
    # Shrink name
    for name in self.name_gen.shrink(user.name):
        shrinks.append(User(name=name, age=user.age, email=user.email))
    
    # Shrink age
    for age in self.age_gen.shrink(user.age):
        shrinks.append(User(name=user.name, age=age, email=user.email))
    
    return shrinks

class JsonGenerator(Generator): """Generate random JSON-compatible data."""

def generate(self, rng: random.Random, size: int) -> Any:
    if size &#x3C;= 0:
        return self._generate_leaf(rng)
    
    choice = rng.choice(['leaf', 'list', 'dict'])
    
    if choice == 'leaf':
        return self._generate_leaf(rng)
    elif choice == 'list':
        length = rng.randint(0, min(size, 5))
        return [self.generate(rng, size - 1) for _ in range(length)]
    else:  # dict
        length = rng.randint(0, min(size, 5))
        return {
            f"key{i}": self.generate(rng, size - 1)
            for i in range(length)
        }

def _generate_leaf(self, rng: random.Random) -> Any:
    choice = rng.choice(['null', 'bool', 'int', 'float', 'string'])
    
    if choice == 'null':
        return None
    elif choice == 'bool':
        return rng.choice([True, False])
    elif choice == 'int':
        return rng.randint(-1000, 1000)
    elif choice == 'float':
        return rng.uniform(-1000, 1000)
    else:
        return ''.join(rng.choices('abcdefghijklmnopqrstuvwxyz', k=rng.randint(0, 10)))

def shrink(self, value: Any) -> List[Any]:
    if value is None or isinstance(value, bool):
        return []
    elif isinstance(value, int):
        return IntGenerator().shrink(value)
    elif isinstance(value, str):
        return StringGenerator().shrink(value)
    elif isinstance(value, list):
        return ListGenerator(self).shrink(value)
    elif isinstance(value, dict):
        shrinks = [{}]  # Empty dict
        for key in value:
            shrinks.append({k: v for k, v in value.items() if k != key})
        return shrinks
    return []

Stateful Testing

class StatefulTest: """ Test stateful systems by generating sequences of operations. Hughes' approach to testing state machines. """

def __init__(self, model_class, system_class):
    self.model_class = model_class
    self.system_class = system_class

def run(self, num_tests: int = 100, max_steps: int = 50):
    """
    Generate sequences of commands and check model matches system.
    """
    rng = random.Random()
    
    for test_num in range(num_tests):
        model = self.model_class()
        system = self.system_class()
        commands = []
        
        for step in range(max_steps):
            # Generate a valid command based on current state
            cmd = self.generate_command(rng, model)
            commands.append(cmd)
            
            # Apply to both model and system
            model_result = cmd.run_model(model)
            system_result = cmd.run_system(system)
            
            # Check they match
            if model_result != system_result:
                shrunk = self.shrink_commands(commands, model_result, system_result)
                raise AssertionError(
                    f"Model/system mismatch!\n"
                    f"Commands: {shrunk}\n"
                    f"Model result: {model_result}\n"
                    f"System result: {system_result}"
                )

Example: Testing a queue

class QueueModel: """Simple model of a queue."""

def __init__(self):
    self.items = []

def push(self, item):
    self.items.append(item)
    return None

def pop(self):
    if self.items:
        return self.items.pop(0)
    return None

def size(self):
    return len(self.items)

class PushCommand: def init(self, item): self.item = item

def run_model(self, model):
    return model.push(self.item)

def run_system(self, system):
    return system.push(self.item)

class PopCommand: def run_model(self, model): return model.pop()

def run_system(self, system):
    return system.pop()

Mental Model

Hughes approaches testing by asking:

  • What properties should always hold? Think invariants, not examples

  • Can I roundtrip it? encode/decode, serialize/deserialize

  • What are the algebraic laws? Monoid, functor, monad laws

  • What's the simplest failing case? Shrinking is essential

  • Am I testing enough cases? Hundreds or thousands, not dozens

The Property-Based Testing Checklist

□ Identify properties that should hold for all inputs □ Build or use appropriate generators □ Ensure generators produce edge cases (empty, large, etc.) □ Implement shrinking for custom generators □ Run sufficient iterations (100+) □ Set seeds for reproducibility □ Analyze shrunk counterexamples carefully □ Complement with example-based tests for specific cases

Signature Hughes Moves

  • Properties over examples

  • Random generation with size control

  • Shrinking to minimal counterexamples

  • Roundtrip testing (encode/decode)

  • Algebraic properties (associativity, identity)

  • Model-based stateful testing

  • Custom generators with shrinking

  • Seed-based reproducibility

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

renaissance-statistical-arbitrage

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

google-material-design

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

aqr-factor-investing

No summary provided by upstream source.

Repository SourceNeeds Review