TS006: HasPropertyTests

Overview

Property Value
ID TS006
Name HasPropertyTests
Group tests
Severity NOTE

Description

Checks if property-based tests are used in the package.

Property-based testing with hypothesis provides more robust test coverage by automatically generating test cases. Instead of writing individual test cases with specific inputs, you describe the properties your code should satisfy, and hypothesis generates hundreds of test cases to verify them.

What it checks

The check looks for evidence of hypothesis usage:

  1. In dependencies: Checks if hypothesis is listed in pyproject.toml optional dependencies (dev, test, etc.)
  2. In test files: Scans tests/ or test/ directories for:
    • from hypothesis imports
    • import hypothesis statements
    • @given decorators
    • @settings decorators

Results

  • PASSED: hypothesis is in dependencies OR hypothesis patterns found in test files
  • FAILED: No evidence of property-based testing found
  • NOT_APPLICABLE: No tests directory found

How to fix

Install hypothesis

Add hypothesis to your dev dependencies:

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "hypothesis>=6.0.0",
]

Install:

pip install -e ".[dev]"

Write property-based tests

Basic example testing a function that reverses strings:

from hypothesis import given, strategies as st

@given(st.text())
def test_reverse_twice_is_identity(s):
    """Reversing a string twice returns the original."""
    assert reverse(reverse(s)) == s

@given(st.text())
def test_reverse_preserves_length(s):
    """Reversing preserves string length."""
    assert len(reverse(s)) == len(s)

Testing with multiple inputs

from hypothesis import given, strategies as st

@given(st.integers(), st.integers())
def test_addition_is_commutative(a, b):
    """Addition is commutative: a + b == b + a."""
    assert a + b == b + a

@given(st.lists(st.integers()))
def test_sorted_list_is_ordered(lst):
    """Sorted list elements are in non-decreasing order."""
    result = sorted(lst)
    for i in range(len(result) - 1):
        assert result[i] <= result[i + 1]

Common strategies

from hypothesis import strategies as st

# Basic types
st.integers()              # Integers
st.floats()                # Floats (includes NaN, inf)
st.text()                  # Unicode strings
st.binary()                # Bytes
st.booleans()              # True/False

# Collections
st.lists(st.integers())    # Lists of integers
st.dictionaries(st.text(), st.integers())  # Dict[str, int]
st.tuples(st.integers(), st.text())        # Tuple[int, str]

# Constrained values
st.integers(min_value=0, max_value=100)    # 0-100
st.text(min_size=1, max_size=10)           # 1-10 chars
st.floats(allow_nan=False, allow_infinity=False)  # Finite floats

Configuration with settings

from hypothesis import given, settings, strategies as st

@given(st.lists(st.integers()))
@settings(max_examples=500)  # Run more examples
def test_with_more_examples(lst):
    assert sorted(lst) == sorted(sorted(lst))

@given(st.text())
@settings(deadline=1000)  # Allow 1 second per example
def test_slow_operation(s):
    result = slow_function(s)
    assert result is not None

Why property-based testing?

Finds edge cases automatically

# Manual test - limited coverage
def test_parse_integer():
    assert parse_int("42") == 42
    assert parse_int("-1") == -1

# Property test - hypothesis finds edge cases you'd miss
@given(st.integers())
def test_parse_integer_roundtrip(n):
    assert parse_int(str(n)) == n
    # Hypothesis may find: large numbers, negative zero, etc.

Reduces test maintenance

Instead of maintaining lists of test cases:

# Before: many manual test cases
@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("Test", "TEST"),
    # ... many more cases
])
def test_uppercase(input, expected):
    assert uppercase(input) == expected

# After: one property test
@given(st.text())
def test_uppercase_properties(s):
    result = uppercase(s)
    assert result == result.upper()
    assert len(result) == len(s)

Configuration

Skip this check

[tool.pycmdcheck]
skip = ["TS006"]

CLI

pycmdcheck --skip TS006

References