Operators¶
Operators define what types of mutations pytest-gremlins can inject into your code. Each operator targets specific code patterns and generates gremlin variants that your tests should catch.
Overview¶
pytest-gremlins ships with five built-in operators, each designed to catch different categories of bugs:
| Operator | Target | Bug Category |
|---|---|---|
comparison |
<, <=, >, >=, ==, != |
Off-by-one errors, boundary bugs |
boundary |
Integer constants in comparisons | Boundary condition bugs |
boolean |
and, or, not, True, False |
Logic errors |
arithmetic |
+, -, *, /, //, %, ** |
Calculation errors |
return |
Return statements | Return value verification |
Built-in Operators¶
comparison¶
The comparison operator mutates comparison operators to catch off-by-one and boundary condition bugs.
What it mutates:
| Original | Mutations |
|---|---|
< |
<=, > |
<= |
<, > |
> |
>=, < |
>= |
>, < |
== |
!= |
!= |
== |
Example mutations:
# Original code
def is_adult(age: int) -> bool:
return age >= 18
# Gremlin 1: >= becomes >
def is_adult(age: int) -> bool:
return age > 18 # Fails for age=18
# Gremlin 2: >= becomes <
def is_adult(age: int) -> bool:
return age < 18 # Completely inverted
When it catches bugs:
- Tests do not check boundary values (e.g., exactly 18)
- Tests do not verify both sides of a condition
- Tests use only extreme values (0, 100) but not edge cases
When to use:
- Always enabled (high-value, low-noise)
- Essential for any code with conditional logic
When to disable:
- Rarely necessary
- Consider disabling if you have many equality checks on strings/enums that produce false positives
boundary¶
The boundary operator shifts integer constants in comparisons by ± 1 to catch off-by-one errors.
What it mutates:
| Original | Mutations |
|---|---|
x >= 18 |
x >= 17, x >= 19 |
x > 0 |
x > -1, x > 1 |
x < 100 |
x < 99, x < 101 |
x == 5 |
x == 4, x == 6 |
Example mutations:
# Original code
def validate_password(password: str) -> bool:
return len(password) >= 8
# Gremlin 1: 8 becomes 7
def validate_password(password: str) -> bool:
return len(password) >= 7 # Accepts 7-char passwords
# Gremlin 2: 8 becomes 9
def validate_password(password: str) -> bool:
return len(password) >= 9 # Rejects 8-char passwords
When it catches bugs:
- Tests do not use boundary values
- Tests skip "fence post" scenarios
- Tests use round numbers but not exact boundaries
When to use:
- Code with numeric thresholds
- Validation logic
- Loop boundaries
When to disable:
- Code with many magic numbers that are not boundaries
- Performance-sensitive mutation testing (generates many gremlins)
To exclude the boundary operator for faster runs:
boolean¶
The boolean operator mutates boolean operators and values to catch logic errors.
What it mutates:
| Original | Mutations |
|---|---|
and |
or |
or |
and |
not x |
x |
True |
False |
False |
True |
Example mutations:
# Original code
def can_access(user):
return user.is_admin and user.is_active
# Gremlin 1: and becomes or
def can_access(user):
return user.is_admin or user.is_active # Security bug!
# Original code
def is_valid():
return True
# Gremlin 2: True becomes False
def is_valid():
return False # Always fails validation
When it catches bugs:
- Tests do not verify all condition combinations
- Tests assume boolean functions return the correct value
- Tests do not check negation logic
When to use:
- Authentication and authorization code
- Feature flags
- Complex conditional logic
When to disable:
- Rarely necessary
- Consider disabling for code with many boolean constants used for configuration
arithmetic¶
The arithmetic operator mutates arithmetic operators to catch calculation errors.
What it mutates:
| Original | Mutations |
|---|---|
+ |
- |
- |
+ |
* |
/ |
/ |
* |
// |
/ |
% |
// |
** |
* |
Example mutations:
# Original code
def calculate_total(price: float, quantity: int) -> float:
return price * quantity
# Gremlin: * becomes /
def calculate_total(price: float, quantity: int) -> float:
return price / quantity # Completely wrong calculation
# Original code
def next_even(n: int) -> int:
return n + n % 2
# Gremlin: % becomes //
def next_even(n: int) -> int:
return n + n // 2 # Wrong result
When it catches bugs:
- Tests do not verify actual calculation results
- Tests only check return type, not value
- Tests use example values that happen to work with multiple operators
When to use:
- Financial calculations
- Scientific computing
- Any code where math correctness matters
When to disable:
- Code with trivial arithmetic (incrementing counters)
- Tests that naturally verify calculations through integration
return¶
The return operator mutates return statements to verify that tests actually check return values.
What it mutates:
| Original | Mutations |
|---|---|
return x |
return None |
return True |
return None, return False |
return False |
return None, return True |
Note: For boolean return values, both return None and the opposite boolean value are generated
as separate gremlins.
Example mutations:
# Original code
def get_user(user_id: int) -> User:
return database.find(user_id)
# Gremlin: return x becomes return None
def get_user(user_id: int) -> User:
return None # Caller may not handle None
# Original code
def is_authenticated() -> bool:
return True
# Gremlin: return True becomes return False
def is_authenticated() -> bool:
return False # Always denies access
When it catches bugs:
- Tests call functions but do not assert on return values
- Tests assume functions work without verifying
- Tests check side effects but not return values
When to use:
- Functions that return important values
- Validation functions
- Data retrieval functions
When to disable:
- Procedures (functions that return nothing meaningful)
- Builder pattern methods (return
self)
Operator Selection Strategies¶
Strategy 1: All Operators (Default)¶
Use all operators for comprehensive mutation testing:
Best for:
- Thorough testing
- CI/CD pipelines with time budget
- Critical codebases
Strategy 2: High-Value Only¶
Focus on operators that catch the most real bugs:
Or via command line:
Best for:
- Quick feedback during development
- Large codebases with time constraints
- First-time mutation testing
Strategy 3: Domain-Specific¶
Choose operators based on your code's domain:
Financial/E-commerce:
Authentication/Authorization:
Data Processing:
Strategy 4: Incremental Adoption¶
Start small and expand:
- Week 1:
comparisononly - Week 2: Add
boolean - Week 3: Add
boundary - Week 4: Add
arithmeticandreturn
# Week 1
pytest --gremlins --gremlin-operators=comparison
# Week 2
pytest --gremlins --gremlin-operators=comparison,boolean
# And so on...
Operator Execution Order¶
When pytest-gremlins runs, operators execute in the order they are registered. The default registration order is:
- comparison
- arithmetic
- boolean
- boundary
- return
When you specify operators via --gremlin-operators or [tool.pytest-gremlins] operators,
they execute in the order you list them.
Understanding Gremlin Output¶
When a gremlin survives, the output shows:
Top surviving gremlins:
src/auth.py:42 >= -> > (comparison)
src/utils.py:17 + -> - (arithmetic)
src/api.py:88 True -> False (return)
Breaking down each line:
| Part | Meaning |
|---|---|
src/auth.py:42 |
File and line number |
>= -> > |
Original code and mutation |
(comparison) |
Operator that created this gremlin |
Equivalent Mutants¶
Some mutations produce identical behavior to the original code. These are called "equivalent mutants" and cannot be caught by any test.
Example:
pytest-gremlins attempts to detect and skip some equivalent mutants, but not all can be detected automatically. A mutation score below 100% is normal and expected.
Writing Tests That Catch Gremlins¶
For comparison operator:¶
Test boundary values explicitly:
# Bad: Only tests happy path
def test_is_adult():
assert is_adult(25) is True
# Good: Tests boundary
def test_is_adult_boundary():
assert is_adult(17) is False
assert is_adult(18) is True
assert is_adult(19) is True
For boolean operator:¶
Test all condition combinations:
# Bad: Only tests one combination
def test_can_access():
admin_user = User(is_admin=True, is_active=True)
assert can_access(admin_user) is True
# Good: Tests all combinations
def test_can_access_combinations():
assert can_access(User(is_admin=True, is_active=True)) is True
assert can_access(User(is_admin=True, is_active=False)) is False
assert can_access(User(is_admin=False, is_active=True)) is False
assert can_access(User(is_admin=False, is_active=False)) is False
For arithmetic operator:¶
Use values that expose wrong operators:
# Bad: 1 * 2 == 2 and 2 + 2 == 4 (different)
def test_multiply():
assert multiply(2, 2) == 4 # Also passes for add!
# Good: Use values that differ for each operator
def test_multiply_distinct():
assert multiply(3, 4) == 12 # 3 + 4 = 7, 3 * 4 = 12
For return operator:¶
Always assert on return values:
# Bad: Calls function without checking return
def test_get_user():
get_user(123) # No assertion!
# Good: Assert on the return value
def test_get_user():
user = get_user(123)
assert user is not None
assert user.id == 123
Custom Operators¶
pytest-gremlins supports custom operators for domain-specific mutations. See the API Reference for details on implementing custom operators.
Example third-party operator:
from pytest_gremlins.operators.registry import OperatorRegistry
# Create a registry instance
registry = OperatorRegistry()
@registry.register_decorator("django-queryset")
class DjangoQuerySetOperator:
"""Mutate Django QuerySet methods."""
@property
def name(self) -> str:
return "django-queryset"
@property
def description(self) -> str:
return "Mutate Django QuerySet methods"
def can_mutate(self, node) -> bool:
# Check for .filter(), .exclude(), etc.
...
def mutate(self, node) -> list:
# Generate mutations
...
Summary¶
| Operator | Use When | Catches |
|---|---|---|
comparison |
Always | Boundary bugs, off-by-one |
boundary |
Numeric thresholds | Fence post errors |
boolean |
Conditional logic | Logic errors |
arithmetic |
Calculations | Math errors |
return |
Functions with return values | Missing assertions |
For most projects, start with all operators enabled and use incremental caching (--gremlin-cache)
to manage performance.