Back to Skills

property-based-testing

aj-geddes
Updated Today
37 views
7
7
View on GitHub
Metatestingautomationdesign

About

This skill helps developers create property-based tests that verify code properties hold across automatically generated inputs rather than specific examples. It's particularly useful for testing algorithms with mathematical properties, verifying invariants, and finding edge cases in parsers, data transformations, and data structures. The approach automatically generates test cases to uncover bugs that traditional example-based testing might miss.

Documentation

Property-Based Testing

Overview

Property-based testing verifies that code satisfies general properties or invariants for a wide range of automatically generated inputs, rather than testing specific examples. This approach finds edge cases and bugs that example-based tests often miss.

When to Use

  • Testing algorithms with mathematical properties
  • Verifying invariants that should always hold
  • Finding edge cases automatically
  • Testing parsers and serializers (round-trip properties)
  • Validating data transformations
  • Testing sorting, searching, and data structure operations
  • Discovering unexpected input combinations

Key Concepts

  • Property: A statement that should be true for all valid inputs
  • Generator: Creates random test inputs
  • Shrinking: Minimizes failing inputs to simplest case
  • Invariant: Condition that must always be true
  • Round-trip: Encoding then decoding returns original value

Instructions

1. Hypothesis for Python

# test_string_operations.py
import pytest
from hypothesis import given, strategies as st, assume, example

def reverse_string(s: str) -> str:
    """Reverse a string."""
    return s[::-1]

class TestStringOperations:
    @given(st.text())
    def test_reverse_twice_returns_original(self, s):
        """Property: Reversing twice returns the original string."""
        assert reverse_string(reverse_string(s)) == s

    @given(st.text())
    def test_reverse_length_unchanged(self, s):
        """Property: Reverse doesn't change length."""
        assert len(reverse_string(s)) == len(s)

    @given(st.text(min_size=1))
    def test_reverse_first_becomes_last(self, s):
        """Property: First char becomes last after reverse."""
        reversed_s = reverse_string(s)
        assert s[0] == reversed_s[-1]
        assert s[-1] == reversed_s[0]

# test_sorting.py
from hypothesis import given, strategies as st

def quick_sort(items):
    """Sort items using quicksort."""
    if len(items) <= 1:
        return items
    pivot = items[len(items) // 2]
    left = [x for x in items if x < pivot]
    middle = [x for x in items if x == pivot]
    right = [x for x in items if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)

class TestSorting:
    @given(st.lists(st.integers()))
    def test_sorted_list_is_ordered(self, items):
        """Property: Every element <= next element."""
        sorted_items = quick_sort(items)
        for i in range(len(sorted_items) - 1):
            assert sorted_items[i] <= sorted_items[i + 1]

    @given(st.lists(st.integers()))
    def test_sorting_preserves_length(self, items):
        """Property: Sorting doesn't add/remove elements."""
        sorted_items = quick_sort(items)
        assert len(sorted_items) == len(items)

    @given(st.lists(st.integers()))
    def test_sorting_preserves_elements(self, items):
        """Property: All elements present in result."""
        sorted_items = quick_sort(items)
        assert sorted(items) == sorted_items

    @given(st.lists(st.integers()))
    def test_sorting_is_idempotent(self, items):
        """Property: Sorting twice gives same result."""
        once = quick_sort(items)
        twice = quick_sort(once)
        assert once == twice

    @given(st.lists(st.integers(), min_size=1))
    def test_sorted_min_at_start(self, items):
        """Property: Minimum element is first."""
        sorted_items = quick_sort(items)
        assert sorted_items[0] == min(items)

    @given(st.lists(st.integers(), min_size=1))
    def test_sorted_max_at_end(self, items):
        """Property: Maximum element is last."""
        sorted_items = quick_sort(items)
        assert sorted_items[-1] == max(items)

# test_json_serialization.py
from hypothesis import given, strategies as st
import json

# Define a strategy for JSON-serializable objects
json_strategy = st.recursive(
    st.none() | st.booleans() | st.integers() | st.floats(allow_nan=False) | st.text(),
    lambda children: st.lists(children) | st.dictionaries(st.text(), children),
    max_leaves=10
)

class TestJSONSerialization:
    @given(json_strategy)
    def test_json_round_trip(self, obj):
        """Property: Encoding then decoding returns original."""
        json_str = json.dumps(obj)
        decoded = json.loads(json_str)
        assert decoded == obj

    @given(st.dictionaries(st.text(), st.integers()))
    def test_json_dict_keys_preserved(self, d):
        """Property: All dictionary keys are preserved."""
        json_str = json.dumps(d)
        decoded = json.loads(json_str)
        assert set(decoded.keys()) == set(d.keys())

# test_math_operations.py
from hypothesis import given, strategies as st, assume
import math

class TestMathOperations:
    @given(st.integers(), st.integers())
    def test_addition_commutative(self, a, b):
        """Property: a + b = b + a"""
        assert a + b == b + a

    @given(st.integers(), st.integers(), st.integers())
    def test_addition_associative(self, a, b, c):
        """Property: (a + b) + c = a + (b + c)"""
        assert (a + b) + c == a + (b + c)

    @given(st.integers())
    def test_addition_identity(self, a):
        """Property: a + 0 = a"""
        assert a + 0 == a

    @given(st.floats(allow_nan=False, allow_infinity=False))
    def test_abs_non_negative(self, x):
        """Property: abs(x) >= 0"""
        assert abs(x) >= 0

    @given(st.floats(allow_nan=False, allow_infinity=False))
    def test_abs_idempotent(self, x):
        """Property: abs(abs(x)) = abs(x)"""
        assert abs(abs(x)) == abs(x)

    @given(st.integers(min_value=0))
    def test_sqrt_inverse_of_square(self, n):
        """Property: sqrt(n^2) = n for non-negative n"""
        assert math.isclose(math.sqrt(n * n), n)

# test_with_examples.py
from hypothesis import given, strategies as st, example

class TestWithExamples:
    @given(st.integers())
    @example(0)  # Ensure we test zero
    @example(-1)  # Ensure we test negative
    @example(1)  # Ensure we test positive
    def test_absolute_value(self, n):
        """Property: abs(n) >= 0, with specific examples."""
        assert abs(n) >= 0

# test_stateful.py
from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
import hypothesis.strategies as st

class StackMachine(RuleBasedStateMachine):
    """Test stack data structure with stateful properties."""

    def __init__(self):
        super().__init__()
        self.stack = []

    @rule(value=st.integers())
    def push(self, value):
        """Push a value onto the stack."""
        self.stack.append(value)

    @rule()
    def pop(self):
        """Pop a value from the stack."""
        if self.stack:
            self.stack.pop()

    @invariant()
    def stack_size_non_negative(self):
        """Invariant: Stack size is never negative."""
        assert len(self.stack) >= 0

    @invariant()
    def peek_equals_last_push(self):
        """Invariant: Peek returns the last pushed value."""
        if self.stack:
            # Last item should be the most recently pushed
            assert self.stack[-1] is not None

TestStack = StackMachine.TestCase

2. fast-check for JavaScript/TypeScript

// string.test.ts
import * as fc from 'fast-check';

describe('String Operations', () => {
  test('reverse twice returns original', () => {
    fc.assert(
      fc.property(fc.string(), (s) => {
        const reversed = s.split('').reverse().join('');
        const doubleReversed = reversed.split('').reverse().join('');
        return s === doubleReversed;
      })
    );
  });

  test('concatenation length', () => {
    fc.assert(
      fc.property(fc.string(), fc.string(), (s1, s2) => {
        return (s1 + s2).length === s1.length + s2.length;
      })
    );
  });

  test('uppercase is idempotent', () => {
    fc.assert(
      fc.property(fc.string(), (s) => {
        const once = s.toUpperCase();
        const twice = once.toUpperCase();
        return once === twice;
      })
    );
  });
});

// array.test.ts
import * as fc from 'fast-check';

function quickSort(arr: number[]): number[] {
  if (arr.length <= 1) return arr;
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);
  return [...quickSort(left), ...middle, ...quickSort(right)];
}

describe('Sorting Properties', () => {
  test('sorted array is ordered', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = quickSort(arr);
        for (let i = 0; i < sorted.length - 1; i++) {
          if (sorted[i] > sorted[i + 1]) return false;
        }
        return true;
      })
    );
  });

  test('sorting preserves length', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        return quickSort(arr).length === arr.length;
      })
    );
  });

  test('sorting preserves elements', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = quickSort(arr);
        const originalSorted = [...arr].sort((a, b) => a - b);
        return JSON.stringify(sorted) === JSON.stringify(originalSorted);
      })
    );
  });

  test('sorting is idempotent', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const once = quickSort(arr);
        const twice = quickSort(once);
        return JSON.stringify(once) === JSON.stringify(twice);
      })
    );
  });
});

// object.test.ts
import * as fc from 'fast-check';

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

const userArbitrary = fc.record({
  id: fc.integer(),
  name: fc.string({ minLength: 1 }),
  email: fc.emailAddress(),
  age: fc.integer({ min: 0, max: 120 }),
});

describe('User Validation', () => {
  test('serialization round trip', () => {
    fc.assert(
      fc.property(userArbitrary, (user) => {
        const json = JSON.stringify(user);
        const parsed = JSON.parse(json);
        return JSON.stringify(parsed) === json;
      })
    );
  });

  test('age validation', () => {
    fc.assert(
      fc.property(userArbitrary, (user) => {
        return user.age >= 0 && user.age <= 120;
      })
    );
  });
});

// custom generators
const positiveIntegerArray = fc.array(fc.integer({ min: 1 }), { minLength: 1 });

test('sum of positives is positive', () => {
  fc.assert(
    fc.property(positiveIntegerArray, (arr) => {
      const sum = arr.reduce((a, b) => a + b, 0);
      return sum > 0;
    })
  );
});

// test with shrinking
test('find minimum failing case', () => {
  try {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        // This will fail for arrays with negative numbers
        return arr.every(n => n >= 0);
      })
    );
  } catch (error) {
    // fast-check will shrink to minimal failing case: [-1] or similar
    console.log('Minimal failing case found:', error);
  }
});

3. junit-quickcheck for Java

// ArrayOperationsTest.java
import com.pholser.junit.quickcheck.Property;
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck;
import com.pholser.junit.quickcheck.generator.InRange;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import java.util.*;

@RunWith(JUnitQuickcheck.class)
public class ArrayOperationsTest {

    @Property
    public void sortingPreservesLength(List<Integer> list) {
        List<Integer> sorted = new ArrayList<>(list);
        Collections.sort(sorted);
        assertEquals(list.size(), sorted.size());
    }

    @Property
    public void sortedListIsOrdered(List<Integer> list) {
        List<Integer> sorted = new ArrayList<>(list);
        Collections.sort(sorted);

        for (int i = 0; i < sorted.size() - 1; i++) {
            assertTrue(sorted.get(i) <= sorted.get(i + 1));
        }
    }

    @Property
    public void sortingIsIdempotent(List<Integer> list) {
        List<Integer> onceSorted = new ArrayList<>(list);
        Collections.sort(onceSorted);

        List<Integer> twiceSorted = new ArrayList<>(onceSorted);
        Collections.sort(twiceSorted);

        assertEquals(onceSorted, twiceSorted);
    }

    @Property
    public void reverseReverseIsIdentity(List<String> list) {
        List<String> once = new ArrayList<>(list);
        Collections.reverse(once);

        List<String> twice = new ArrayList<>(once);
        Collections.reverse(twice);

        assertEquals(list, twice);
    }
}

// StringOperationsTest.java
@RunWith(JUnitQuickcheck.class)
public class StringOperationsTest {

    @Property
    public void concatenationLength(String s1, String s2) {
        assertEquals(s1.length() + s2.length(), (s1 + s2).length());
    }

    @Property
    public void uppercaseIsIdempotent(String s) {
        String once = s.toUpperCase();
        String twice = once.toUpperCase();
        assertEquals(once, twice);
    }

    @Property
    public void trimRemovesWhitespace(String s) {
        String trimmed = s.trim();
        if (!trimmed.isEmpty()) {
            assertFalse(Character.isWhitespace(trimmed.charAt(0)));
            assertFalse(Character.isWhitespace(trimmed.charAt(trimmed.length() - 1)));
        }
    }
}

// MathOperationsTest.java
@RunWith(JUnitQuickcheck.class)
public class MathOperationsTest {

    @Property
    public void additionCommutative(int a, int b) {
        assertEquals(a + b, b + a);
    }

    @Property
    public void additionAssociative(int a, int b, int c) {
        assertEquals((a + b) + c, a + (b + c));
    }

    @Property
    public void absoluteValueNonNegative(int n) {
        assertTrue(Math.abs(n) >= 0);
    }

    @Property
    public void absoluteValueIdempotent(int n) {
        assertEquals(Math.abs(n), Math.abs(Math.abs(n)));
    }

    @Property
    public void divisionByNonZero(
        int dividend,
        @InRange(minInt = 1, maxInt = Integer.MAX_VALUE) int divisor
    ) {
        int result = dividend / divisor;
        assertTrue(result * divisor <= dividend + divisor);
    }
}

Common Properties to Test

Universal Properties

  • Idempotence: f(f(x)) = f(x)
  • Identity: f(x, identity) = x
  • Commutativity: f(a, b) = f(b, a)
  • Associativity: f(f(a, b), c) = f(a, f(b, c))
  • Inverse: f(inverse_f(x)) = x

Data Structure Properties

  • Round-trip: decode(encode(x)) = x
  • Preservation: Operation preserves length, elements, or structure
  • Ordering: Elements maintain required order
  • Bounds: Values stay within valid ranges
  • Invariants: Class invariants always hold

Best Practices

✅ DO

  • Focus on general properties, not specific cases
  • Test mathematical properties (commutativity, associativity)
  • Verify round-trip encoding/decoding
  • Use shrinking to find minimal failing cases
  • Combine with example-based tests for known edge cases
  • Test invariants that should always hold
  • Generate realistic input distributions

❌ DON'T

  • Test properties that are tautologies
  • Over-constrain input generation
  • Ignore shrunk test failures
  • Replace all example tests with properties
  • Test implementation details
  • Generate invalid inputs without constraints
  • Forget to handle edge cases in generators

Tools & Libraries

  • Python: Hypothesis
  • JavaScript/TypeScript: fast-check, jsverify
  • Java: junit-quickcheck, jqwik
  • Scala: ScalaCheck
  • Haskell: QuickCheck (original)
  • C#: FsCheck

Examples

See also: test-data-generation, mutation-testing, continuous-testing for comprehensive testing strategies.

Quick Install

/plugin add https://github.com/aj-geddes/useful-ai-prompts/tree/main/property-based-testing

Copy and paste this command in Claude Code to install this skill

GitHub 仓库

aj-geddes/useful-ai-prompts
Path: skills/property-based-testing

Related Skills

sglang

Meta

SGLang is a high-performance LLM serving framework that specializes in fast, structured generation for JSON, regex, and agentic workflows using its RadixAttention prefix caching. It delivers significantly faster inference, especially for tasks with repeated prefixes, making it ideal for complex, structured outputs and multi-turn conversations. Choose SGLang over alternatives like vLLM when you need constrained decoding or are building applications with extensive prefix sharing.

View skill

evaluating-llms-harness

Testing

This Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.

View skill

langchain

Meta

LangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.

View skill

Algorithmic Art Generation

Meta

This skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.

View skill