Skip to main content
โšก Calmops

Property-Based Testing: Finding Bugs with Random Data

Introduction

Traditional testing uses specific examples. Property-based testing throws thousands of random inputs at your code, finding bugs you’d never think to test. This guide covers property-based testing with fast-check and Hypothesis.


Traditional vs Property-Based

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚            Traditional vs Property-Based Testing               โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚  TRADITIONAL (Example-Based):                               โ”‚
โ”‚                                                             โ”‚
โ”‚  test('sort works', () => {                               โ”‚
โ”‚    expect(sort([3, 1, 2])).toEqual([1, 2, 3]);           โ”‚
โ”‚  });                                                       โ”‚
โ”‚                                                             โ”‚
โ”‚  โœ“ Tests specific cases                                    โ”‚
โ”‚  โœ“ Easy to understand                                      โ”‚
โ”‚  โœ— May miss edge cases                                     โ”‚
โ”‚                                                             โ”‚
โ”‚  PROPERTY-BASED:                                           โ”‚
โ”‚                                                             โ”‚
โ”‚  test('sort is correct', () => {                          โ”‚
โ”‚    fc.assert(fc.property(fc.array(fc.integer()), arr => { โ”‚
โ”‚      const sorted = sort(arr);                             โ”‚
โ”‚      return isSorted(sorted) && sameElements(arr, sorted);โ”‚
โ”‚    }));                                                    โ”‚
โ”‚  });                                                       โ”‚
โ”‚                                                             โ”‚
โ”‚  โœ“ Tests thousands of random cases                        โ”‚
โ”‚  โœ“ Finds edge cases automatically                         โ”‚
โ”‚  โœ“ More confidence in correctness                          โ”‚
โ”‚                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Concepts

Properties

A property is a statement that’s always true:

properties:
  - "Sorting a list produces a sorted list"
  - "Reversing twice returns original"
  - "Adding two numbers equals adding in any order"
  - "JSON.parse(JSON.stringify(x)) equals x"

fast-check (JavaScript/TypeScript)

Setup

npm install fast-check

Basic Example

import { fc, test } from 'fast-check';

// Property: Sorting produces sorted output
test('sort produces sorted array', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      const sorted = [...arr].sort((a, b) => a - b);
      
      // Check sorted
      for (let i = 1; i < sorted.length; i++) {
        if (sorted[i] < sorted[i - 1]) {
          return false;
        }
      }
      return true;
    })
  );
});

// Property: Reversing twice returns original
test('reverse twice returns original', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      const reversed = [...arr].reverse();
      const doubleReversed = [...reversed].reverse();
      return JSON.stringify(arr) === JSON.stringify(doubleReversed);
    })
  );
});

Custom Arbitraries

// Generate valid email addresses
const emailArb = fc.record({
  username: fc.string({ minLength: 1 }),
  domain: fc.oneof(
    fc.constant('gmail.com'),
    fc.constant('outlook.com'),
    fc.constant('company.com')
  ),
}).map(({ username, domain }) => `${username}@${domain}`);

fc.assert(
  fc.property(emailArb, (email) => {
    return isValidEmail(email); // Your validation function
  })
);

// Generate valid JSON
const jsonValueArb: fc.Arbitrary<unknown> = fc.oneof(
  fc.constant(null),
  fc.boolean(),
  fc.integer(),
  fc.string(),
  fc.array(jsonValueArb),
  fc.dictionary(jsonValueArb)
);

Filtering and Shrinking

// Generate non-empty arrays
test('sort non-empty arrays', () => {
  fc.assert(
    fc.property(
      fc.array(fc.integer(), { minLength: 1 }),
      (arr) => {
        return sort(arr).length > 0;
      }
    )
  );
});

// Filter invalid inputs
test('parse valid JSON', () => {
  fc.assert(
    fc.property(
      fc.string().filter(s => {
        try { JSON.parse(s); return true; }
        catch { return false; }
      }),
      (str) => {
        const parsed = JSON.parse(str);
        return typeof parsed === 'object';
      }
    )
  );
});

Testing API Functions

Database Operations

import { fc } from 'fast-check';

// Property: Creating user with valid data succeeds
test('createUser accepts valid input', () => {
  fc.assert(
    fc.property(
      fc.record({
        name: fc.string({ minLength: 1, maxLength: 100 }),
        email: fc.emailAddress(),
        age: fc.integer({ min: 0, max: 150 }),
      }),
      async (userData) => {
        const user = await createUser(userData);
        return user.name === userData.name &&
               user.email === userData.email;
      }
    )
  );
});

Business Logic

// Property: Discount never exceeds original price
test('discount is valid', () => {
  fc.assert(
    fc.property(
      fc.float({ min: 0.01, max: 10000 }),
      fc.float({ min: 0, max: 100 }),
      (price, discount) => {
        const finalPrice = calculateDiscount(price, discount);
        return finalPrice >= 0 && finalPrice <= price;
      }
    )
  );
});

Hypothesis (Python)

Setup

pip install hypothesis

Basic Example

from hypothesis import given, settings, Verbosity
import hypothesis.strategies as st

@given(st.lists(st.integers()))
def test_sort_returns_sorted(arr):
    sorted_arr = sorted(arr)
    # Check each element is in order
    for i in range(len(sorted_arr) - 1):
        assert sorted_arr[i] <= sorted_arr[i + 1]

@given(st.lists(st.integers(), min_size=1))
def test_sort_preserves_length(arr):
    sorted_arr = sorted(arr)
    assert len(sorted_arr) == len(arr)

Advanced Strategies

from hypothesis import given, st
import re

# Custom strategies
@given(st.from_regex(r"[a-z]+@[a-z]+\.(com|org|net)"))
def test_valid_email_format(email):
    assert re.match(r"[a-z]+@[a-z]+\.(com|org|net)", email)

# Dependent generation
@given(
    data=st.data()
)
def test_list_operations(data):
    # Generate list size
    size = data.draw(st.integers(min_value=1, max_value=100))
    arr = data.draw(st.lists(st.integers(), min_size=size, max_size=size))
    
    # Test property
    assert len(sorted(arr)) == size

Key Takeaways

  • Property-based testing - Tests thousands of random cases
  • Finds edge cases - Bugs you’d never manually test
  • Great for - Pure functions, data transformations, API validation
  • Not great for - UI interactions, complex integrations

External Resources

Comments