본문으로 건너뛰기
cccskills
무료GitHub에 공개

property-based-testing

Build tests with fast-check for TypeScript and JavaScript or Hypothesis for Python. Generate varied inputs to check rules code should always satisfy, discover edge cases, and simplify failing inputs into small examples for debugging.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

원문원문 설명 보기

Property-based testing with fast-check (TypeScript/JavaScript) and Hypothesis (Python). Generate test cases automatically, find edge cases, and test mathematical properties. Use when user mentions property-based testing, fast-check, Hypothesis, generating test data, QuickCheck-style testing, or finding edge cases automatically.

설치 방법 보기

이럴 때 유용합니다

  • Checking that encoding and decoding restore the original data
  • Testing rules that sorting must preserve
  • Generating inputs with complex constraints
  • Testing shopping cart behavior across operation sequences

포함된 파일(1)

  • SKILL.md18.6 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Property-Based Testing

Expert knowledge for property-based testing - automatically generating test cases to verify code properties rather than testing specific examples.

Core Expertise

Property-Based Testing Concept

  • Traditional testing: Test specific examples
  • Property-based testing: Test properties that should hold for all inputs
  • Generators: Automatically create diverse test inputs
  • Shrinking: Minimize failing cases to simplest example
  • Coverage: Explore edge cases humans might miss

When to Use Property-Based Testing

  • Mathematical operations (commutative, associative properties)
  • Encoders/decoders (roundtrip properties)
  • Parsers and serializers
  • Data transformations
  • API contracts
  • Invariants and constraints

TypeScript/JavaScript (fast-check)

Installation

# Using Bun
bun add -d fast-check

# Using npm
npm install -D fast-check

Basic Example

import { test } from 'vitest'
import * as fc from 'fast-check'

// Traditional example-based test
test('reverse twice returns original', () => {
  expect(reverse(reverse([1, 2, 3]))).toEqual([1, 2, 3])
})

// Property-based test
test('reverse twice returns original - property based', () => {
  fc.assert(
    fc.property(
      fc.array(fc.integer()), // Generate random arrays of integers
      (arr) => {
        expect(reverse(reverse(arr))).toEqual(arr)
      }
    )
  )
})
// fast-check automatically generates 100s of test cases!

Built-in Generators

import * as fc from 'fast-check'

// Numbers
fc.integer()                          // Any integer
fc.integer({ min: 0, max: 100 })      // Range
fc.nat()                              // Natural numbers (≥ 0)
fc.float()                            // Floating-point
fc.double()                           // Double precision

// Strings
fc.string()                           // Any string
fc.string({ minLength: 1, maxLength: 10 })
fc.hexaString()                       // Hex strings
fc.asciiString()                      // ASCII only
fc.unicodeString()                    // Unicode
fc.emailAddress()                     // Email format

// Arrays and Objects
fc.array(fc.integer())                // Array of integers
fc.array(fc.string(), { minLength: 1, maxLength: 5 })
fc.set(fc.integer())                  // Unique values
fc.record({                           // Objects
  name: fc.string(),
  age: fc.nat(),
})

// Booleans and Constants
fc.boolean()
fc.constant('value')
fc.constantFrom('a', 'b', 'c')        // Pick from options

// Dates
fc.date()
fc.date({ min: new Date('2020-01-01') })

// Complex Types
fc.tuple(fc.string(), fc.integer())   // Fixed-size tuple
fc.oneof(fc.string(), fc.integer())   // Union type
fc.option(fc.string())                // string | null

Custom Generators

// Generate user objects
const userArbitrary = fc.record({
  id: fc.nat(),
  name: fc.string({ minLength: 1, maxLength: 50 }),
  email: fc.emailAddress(),
  age: fc.integer({ min: 18, max: 120 }),
  roles: fc.array(fc.constantFrom('admin', 'user', 'guest'), {
    minLength: 1,
    maxLength: 3,
  }),
})

test('user validation properties', () => {
  fc.assert(
    fc.property(userArbitrary, (user) => {
      const validated = validateUser(user)
      expect(validated.age).toBeGreaterThanOrEqual(18)
      expect(validated.name.length).toBeGreaterThan(0)
      expect(validated.roles.length).toBeGreaterThan(0)
    })
  )
})

// Generate using map
const positiveNumberArbitrary = fc.nat().map((n) => n + 1)

// Generate using chain (dependent values)
const emailAndDomainArbitrary = fc.string().chain((domain) =>
  fc.record({
    email: fc.constant(`user@${domain}.com`),
    domain: fc.constant(domain),
  })
)

Common Properties to Test

Roundtrip Property (Encode/Decode)

test('JSON serialization roundtrip', () => {
  fc.assert(
    fc.property(
      fc.record({
        name: fc.string(),
        age: fc.nat(),
        tags: fc.array(fc.string()),
      }),
      (obj) => {
        const serialized = JSON.stringify(obj)
        const deserialized = JSON.parse(serialized)
        expect(deserialized).toEqual(obj)
      }
    )
  )
})

Idempotence (f(f(x)) = f(x))

test('sort is idempotent', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      const sorted = sort(arr)
      const doubleSorted = sort(sorted)
      expect(doubleSorted).toEqual(sorted)
    })
  )
})

Commutativity (f(a, b) = f(b, a))

test('addition is commutative', () => {
  fc.assert(
    fc.property(fc.integer(), fc.integer(), (a, b) => {
      expect(add(a, b)).toBe(add(b, a))
    })
  )
})

Associativity ((a + b) + c = a + (b + c))

test('addition is associative', () => {
  fc.assert(
    fc.property(fc.integer(), fc.integer(), fc.integer(), (a, b, c) => {
      expect(add(add(a, b), c)).toBe(add(a, add(b, c)))
    })
  )
})

Identity (f(x, identity) = x)

test('multiplication identity', () => {
  fc.assert(
    fc.property(fc.integer(), (n) => {
      expect(multiply(n, 1)).toBe(n)
    })
  )
})

Inverse (f(g(x)) = x)

test('encryption/decryption inverse', () => {
  fc.assert(
    fc.property(fc.string(), fc.string(), (plaintext, key) => {
      const encrypted = encrypt(plaintext, key)
      const decrypted = decrypt(encrypted, key)
      expect(decrypted).toBe(plaintext)
    })
  )
})

Shrinking (Simplifying Failing Cases)

// When a property fails, fast-check automatically shrinks
// the input to the minimal failing case

test('finds minimal failing case', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      // This will fail for arrays containing 42
      expect(arr).not.toContain(42)
    })
  )
})

// Output:
// Property failed after 1 tests
// Shrunk 5 time(s)
// Counterexample: [[42]]  ← Minimal failing case!

Configuration

test('configured property test', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      expect(sort(arr)).toBeSorted()
    }),
    {
      numRuns: 1000,      // Run 1000 tests (default: 100)
      seed: 42,           // Reproducible tests
      endOnFailure: true, // Stop after first failure
      verbose: true,      // Show all generated values
    }
  )
})

Preconditions (Filtering)

test('division properties for non-zero divisors', () => {
  fc.assert(
    fc.property(fc.integer(), fc.integer(), (a, b) => {
      fc.pre(b !== 0) // Skip cases where b is 0

      const result = divide(a, b)
      expect(multiply(result, b)).toBeCloseTo(a)
    })
  )
})

Python (Hypothesis)

Installation

# Using uv
uv add --dev hypothesis

# Using pip
pip install hypothesis

Basic Example

from hypothesis import given, strategies as st
import pytest

# Traditional example-based test
def test_reverse_twice_example():
    assert reverse(reverse([1, 2, 3])) == [1, 2, 3]

# Property-based test
@given(st.lists(st.integers()))
def test_reverse_twice_property(arr):
    assert reverse(reverse(arr)) == arr
    # Hypothesis automatically generates 100s of test cases!

Built-in Strategies

from hypothesis import strategies as st

# Numbers
st.integers()                          # Any integer
st.integers(min_value=0, max_value=100)
st.floats()                            # Floating-point
st.floats(min_value=0.0, max_value=1.0, allow_nan=False)
st.decimals()                          # Decimal precision

# Strings
st.text()                              # Any string
st.text(min_size=1, max_size=10)
st.text(alphabet='abc')                # Limited alphabet
st.binary()                            # Bytes

# Collections
st.lists(st.integers())                # List of integers
st.lists(st.text(), min_size=1, max_size=5)
st.sets(st.integers())                 # Unique values
st.dictionaries(keys=st.text(), values=st.integers())

# Booleans and Constants
st.booleans()
st.just('value')                       # Constant
st.sampled_from(['a', 'b', 'c'])      # Pick from options

# Dates and Times
st.dates()
st.datetimes()
st.times()
st.timedeltas()

# Complex Types
st.tuples(st.text(), st.integers())   # Fixed-size tuple
st.one_of(st.text(), st.integers())   # Union type

Custom Strategies

from hypothesis import strategies as st
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str
    age: int

# Strategy for generating users
users = st.builds(
    User,
    id=st.integers(min_value=1),
    name=st.text(min_size=1, max_size=50),
    email=st.emails(),
    age=st.integers(min_value=18, max_value=120),
)

@given(users)
def test_user_validation(user):
    validated = validate_user(user)
    assert validated.age >= 18
    assert len(validated.name) > 0
# Using map
positive_numbers = st.integers(min_value=0).map(lambda n: n + 1)

# Using flatmap (dependent values)
@st.composite
def email_and_domain(draw):
    domain = draw(st.text(min_size=1))
    return {
        'email': f'user@{domain}.com',
        'domain': domain,
    }

Common Properties to Test

Roundtrip Property

import json
from hypothesis import given, strategies as st

@given(st.dictionaries(
    keys=st.text(),
    values=st.one_of(st.integers(), st.text(), st.booleans())
))
def test_json_roundtrip(obj):
    serialized = json.dumps(obj)
    deserialized = json.loads(serialized)
    assert deserialized == obj

Idempotence

@given(st.lists(st.integers()))
def test_sort_idempotent(arr):
    sorted_once = sorted(arr)
    sorted_twice = sorted(sorted_once)
    assert sorted_once == sorted_twice

Commutativity

@given(st.integers(), st.integers())
def test_addition_commutative(a, b):
    assert add(a, b) == add(b, a)

Associativity

@given(st.integers(), st.integers(), st.integers())
def test_addition_associative(a, b, c):
    assert add(add(a, b), c) == add(a, add(b, c))

Identity

@given(st.integers())
def test_multiplication_identity(n):
    assert multiply(n, 1) == n

Inverse

@given(st.text(), st.text(min_size=1))
def test_encryption_inverse(plaintext, key):
    encrypted = encrypt(plaintext, key)
    decrypted = decrypt(encrypted, key)
    assert decrypted == plaintext

Shrinking (Simplifying Failing Cases)

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_finds_minimal_failing_case(arr):
    # This will fail for arrays containing 42
    assert 42 not in arr

# Output:
# Falsifying example: test_finds_minimal_failing_case(
#     arr=[42]  ← Minimal failing case!
# )

Configuration and Settings

from hypothesis import given, settings, strategies as st

@settings(max_examples=1000, deadline=None)
@given(st.lists(st.integers()))
def test_with_custom_settings(arr):
    assert sort(arr) == sorted(arr)

# Global settings
from hypothesis import settings, Verbosity

settings.register_profile("ci", max_examples=1000, verbosity=Verbosity.verbose)
settings.register_profile("dev", max_examples=100)
settings.load_profile("dev")

Assumptions (Preconditions)

from hypothesis import given, assume, strategies as st

@given(st.integers(), st.integers())
def test_division_properties(a, b):
    assume(b != 0)  # Skip cases where b is 0

    result = divide(a, b)
    assert abs(multiply(result, b) - a) < 0.0001

Stateful Testing

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

class ShoppingCartMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.cart = ShoppingCart()
        self.items = []

    @rule(item=st.text(min_size=1), price=st.floats(min_value=0.01, max_value=1000))
    def add_item(self, item, price):
        self.cart.add(item, price)
        self.items.append((item, price))

    @rule()
    def clear_cart(self):
        self.cart.clear()
        self.items = []

    @invariant()
    def total_matches_items(self):
        expected_total = sum(price for _, price in self.items)
        assert abs(self.cart.total() - expected_total) < 0.01

# Run stateful test
TestCart = ShoppingCartMachine.TestCase

Real-World Examples

TypeScript: URL Parser

import * as fc from 'fast-check'

test('URL parsing roundtrip', () => {
  fc.assert(
    fc.property(
      fc.webUrl(), // Built-in URL generator
      (url) => {
        const parsed = parseURL(url)
        const reconstructed = buildURL(parsed)
        expect(normalizeURL(reconstructed)).toBe(normalizeURL(url))
      }
    )
  )
})

Python: Data Validation

from hypothesis import given, strategies as st
from pydantic import BaseModel, ValidationError

class Product(BaseModel):
    name: str
    price: float
    quantity: int

@given(st.builds(
    Product,
    name=st.text(min_size=1),
    price=st.floats(min_value=0.01, max_value=10000),
    quantity=st.integers(min_value=0, max_value=1000),
))
def test_product_validation_accepts_valid_data(product):
    # Should not raise
    validated = Product(**product.dict())
    assert validated.price > 0
    assert validated.quantity >= 0

TypeScript: List Operations

test('filter and map compose correctly', () => {
  fc.assert(
    fc.property(
      fc.array(fc.integer()),
      fc.func(fc.boolean()),
      fc.func(fc.integer()),
      (arr, predicate, transform) => {
        const result1 = arr.filter(predicate).map(transform)
        const result2 = arr.map(transform).filter((_, i) =>
          predicate(arr[i])
        )
        // Order might differ but length should match
        expect(result1.length).toBe(result2.length)
      }
    )
  )
})

Python: Cache Behavior

from hypothesis import given, strategies as st

@given(st.text(), st.integers())
def test_cache_returns_same_value(key, value):
    cache = Cache()

    # First set
    cache.set(key, value)
    result1 = cache.get(key)

    # Second get should return same value
    result2 = cache.get(key)

    assert result1 == value
    assert result2 == value

Best Practices

Start with Properties

  • Identify mathematical properties (commutative, associative)
  • Look for roundtrip properties (encode/decode)
  • Test invariants (things that should always be true)
  • Verify contracts and postconditions

Complement Example-Based Tests

// Use both approaches
test('addition examples', () => {
  expect(add(2, 3)).toBe(5)
  expect(add(-1, 1)).toBe(0)
})

test('addition properties', () => {
  fc.assert(
    fc.property(fc.integer(), fc.integer(), (a, b) => {
      expect(add(a, b)).toBe(add(b, a)) // Commutative
      expect(add(a, 0)).toBe(a)          // Identity
    })
  )
})

Shrinking is Your Friend

  • Don't ignore shrunk counterexamples
  • Minimal failing cases reveal root causes
  • Shrinking finds edge cases you'd never write by hand

Performance Considerations

// Limit expensive tests
fc.assert(
  fc.property(fc.array(fc.integer()), (arr) => {
    expensiveOperation(arr)
  }),
  { numRuns: 50 } // Reduce from default 100
)

Reproducibility

# Set seed for reproducible failures
@settings(derandomize=True)
@given(st.lists(st.integers()))
def test_reproducible(arr):
    assert process(arr) is not None

Common Pitfalls

Overly Permissive Assertions

// ❌ BAD: Too weak
fc.assert(
  fc.property(fc.array(fc.integer()), (arr) => {
    expect(sort(arr)).toBeDefined() // Passes even if sort is broken!
  })
)

// ✅ GOOD: Specific properties
fc.assert(
  fc.property(fc.array(fc.integer()), (arr) => {
    const sorted = sort(arr)
    // Check actual properties
    for (let i = 1; i < sorted.length; i++) {
      expect(sorted[i]).toBeGreaterThanOrEqual(sorted[i - 1])
    }
  })
)

Too Many Assumptions

# ❌ BAD: Filters out too many cases
@given(st.integers(), st.integers())
def test_slow(a, b):
    assume(a > 100)
    assume(a < 110)
    assume(b > 200)
    assume(b < 210)
    # Better to use specific strategy!

# ✅ GOOD: Generate what you need
@given(st.integers(min_value=101, max_value=109),
       st.integers(min_value=201, max_value=209))
def test_fast(a, b):
    # No filtering needed

Testing Implementation, Not Properties

// ❌ BAD: Tests implementation
fc.assert(
  fc.property(fc.array(fc.integer()), (arr) => {
    const spy = vi.spyOn(Math, 'max')
    sort(arr)
    expect(spy).toHaveBeenCalled() // Testing how it's implemented
  })
)

// ✅ GOOD: Tests properties
fc.assert(
  fc.property(fc.array(fc.integer()), (arr) => {
    const sorted = sort(arr)
    // Test what it does, not how
    expect(sorted.length).toBe(arr.length)
    expect(new Set(sorted)).toEqual(new Set(arr))
  })
)

CI/CD Integration

TypeScript

{
  "scripts": {
    "test": "vitest",
    "test:property": "vitest --grep 'property'",
    "test:ci": "vitest --run --coverage"
  }
}

Python

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v1
      - run: uv sync
      - run: uv run pytest --hypothesis-show-statistics

Troubleshooting

Tests taking too long

// Reduce number of runs
fc.assert(property, { numRuns: 50 })
@settings(max_examples=50)
@given(...)

Hard to find failing case

// Increase attempts
fc.assert(property, { numRuns: 10000 })

Flaky property tests

# Use seed for reproducibility
@settings(derandomize=True)

Too many filtered cases

Hypothesis: Unable to satisfy assumptions

→ Use more specific generators instead of assume()

See Also

  • vitest-testing - Unit testing framework
  • python-testing - Python pytest testing
  • test-quality-analysis - Detecting test smells
  • mutation-testing - Validate test effectiveness

References

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

adaptyv

무료

Support experimental validation of designed proteins through a cloud laboratory. Prepare and optimize sequences, submit tests for binding, expression, thermal stability, or enzyme activity, track experiment progress, and retrieve results through the API.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Submit designed proteins for binding tests
  • Request expression, thermal stability, or enzyme activity tests
  • Optimize sequences for solubility before experiments
foryourhealth111-pixel/Vibe-Skills3.8천2026년 10월 23일 업데이트

aeon

무료

Analyze time series, observations ordered over time, using aeon in Python. Build classification, prediction, and clustering workflows, detect unusual patterns or changes, and compare methods through an interface compatible with scikit-learn.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Classifying time series by their patterns
  • Forecasting future values from past observations
  • Detecting anomalies and change points
foryourhealth111-pixel/Vibe-Skills3.8천2026년 10월 23일 업데이트

Create original generative art with p5.js, using code to produce patterns and motion. Receive a written artistic concept and an interactive HTML viewer with adjustable parameters, reproducible random variations, seed navigation, and PNG downloads.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Create art from particle motion and flow fields
  • Explore patterns by adjusting sliders
  • Reproduce a favorite variation with the same seed
foryourhealth111-pixel/Vibe-Skills3.8천2026년 10월 23일 업데이트

Retrieve stock prices, exchange rates, cryptocurrency prices, company financial statements, and economic indicators through the Alpha Vantage API. It also covers historical data, news sentiment, and technical indicators, which summarize price and trading patterns.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Retrieve historical stock prices and trading volumes
  • Collect company financial statements and earnings data
  • Check foreign exchange and cryptocurrency prices
foryourhealth111-pixel/Vibe-Skills3.8천2026년 10월 23일 업데이트

Design backend systems with clear boundaries between business rules, databases, and external services. Use Clean Architecture, Hexagonal Architecture, and Domain-Driven Design to organize new applications, refactor existing code, and make core logic easier to test.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Planning the structure of a new backend
  • Refactoring tightly coupled applications
  • Testing business rules independently of external services
foryourhealth111-pixel/Vibe-Skills3.8천2026년 10월 23일 업데이트

astropy

무료

Helps process astronomical data in Python by converting coordinates, units, and time scales, working with FITS observation files, matching object catalogs, and calculating distances using cosmological models.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Converting celestial coordinates
  • Reading and editing FITS observation files
  • Matching objects across astronomical catalogs
foryourhealth111-pixel/Vibe-Skills3.8천2026년 10월 23일 업데이트

foryourhealth111-pixel의 스킬 모두 보기

이 스킬의 문제 신고