pytest Plugin Compatibility¶
Configure pytest-gremlins to work alongside other popular pytest plugins.
Goal¶
Integrate pytest-gremlins with pytest-cov, pytest-xdist, and other plugins without conflicts.
Prerequisites¶
- pytest-gremlins installed
- One or more additional pytest plugins
- Understanding of each plugin's purpose
pytest-cov Integration¶
Goal¶
Run coverage collection and mutation testing together, or separately, without conflicts.
Configuration¶
Create pyproject.toml:
[project]
name = "myproject"
version = "1.0.0"
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.1.0",
"pytest-gremlins>=1.0.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
# Coverage configuration
[tool.coverage.run]
source = ["src"]
branch = true
parallel = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
fail_under = 80
# Gremlins configuration
[tool.pytest-gremlins]
paths = ["src"]
Running Both Tools¶
Option 1: Run separately (recommended)¶
# Run coverage first
pytest tests/ --cov=src --cov-report=html --cov-report=term
# Run mutation testing second
pytest --gremlins --gremlin-report=html
Option 2: Run together¶
CI Workflow¶
Create .github/workflows/quality.yml:
name: Code Quality
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Run tests with coverage
run: pytest tests/ --cov=src --cov-report=xml --cov-report=term
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: coverage.xml
mutation:
name: Mutation Testing
runs-on: ubuntu-latest
needs: coverage # Run after coverage passes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Run mutation testing
run: pytest --gremlins --gremlin-report=html
- name: Upload mutation report
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: coverage/gremlins/
Verification¶
- Run coverage and verify report:
- Run mutation testing:
- Both should complete without errors
Troubleshooting¶
Coverage reports are empty when running with gremlins¶
pytest-gremlins may interfere with coverage collection. Run separately:
CoverageWarning: No data was collected¶
Ensure source paths match:
[tool.coverage.run]
source = ["src"] # Must match your package location
[tool.pytest-gremlins]
paths = ["src"] # Same path
pytest-xdist Integration¶
Goal¶
Use -n from pytest-xdist to control parallel mutation testing.
How It Works¶
pytest-gremlins reads xdist's -n flag and uses it as the worker count for its
mutation subprocess pool. Pass -n auto to use all CPU cores, or -n 4 for
an explicit count.
Configuration¶
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-xdist>=3.5.0",
"pytest-gremlins>=1.0.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
[tool.pytest-gremlins]
paths = ["src"]
Running Tests¶
Mutation testing with automatic worker count:
Mutation testing with explicit worker count:
In CI:
jobs:
mutation:
steps:
- name: Run mutation testing
run: pytest --gremlins -n auto --gremlin-cache --gremlin-report=html
Verification¶
The output will show Starting parallel execution with 4 workers.
Troubleshooting¶
Worker processes crash¶
Reduce the worker count:
pytest-bdd Integration¶
Goal¶
Run mutation testing alongside BDD-style tests written with pytest-bdd.
Configuration¶
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-bdd>=7.0.0",
"pytest-gremlins>=1.0.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
bdd_features_base_dir = "tests/features"
[tool.pytest-gremlins]
paths = ["src"]
# Exclude step definitions from mutation (they're test code)
exclude = [
"**/steps/*",
"**/conftest.py",
]
Project Structure¶
myproject/
├── src/
│ └── myapp/
│ └── calculator.py
├── tests/
│ ├── features/
│ │ └── calculator.feature
│ ├── steps/
│ │ └── test_calculator_steps.py
│ └── conftest.py
└── pyproject.toml
Example Feature¶
Create tests/features/calculator.feature:
Feature: Calculator
As a user
I want to perform calculations
So that I get accurate results
Scenario: Add two numbers
Given I have a calculator
When I add 2 and 3
Then the result is 5
Scenario: Subtract two numbers
Given I have a calculator
When I subtract 3 from 10
Then the result is 7
Scenario: Divide by zero
Given I have a calculator
When I divide 10 by 0
Then I get a division error
Example Steps¶
Create tests/steps/test_calculator_steps.py:
"""Step definitions for calculator feature."""
import pytest
from pytest_bdd import scenarios, given, when, then, parsers
from myapp.calculator import Calculator
scenarios('../features/calculator.feature')
@pytest.fixture
def calculator():
"""Create a calculator instance."""
return Calculator()
@pytest.fixture
def result():
"""Container for calculation result."""
return {'value': None, 'error': None}
@given('I have a calculator')
def have_calculator(calculator):
"""Calculator is available."""
assert calculator is not None
@when(parsers.parse('I add {a:d} and {b:d}'))
def add_numbers(calculator, result, a, b):
"""Add two numbers."""
result['value'] = calculator.add(a, b)
@when(parsers.parse('I subtract {b:d} from {a:d}'))
def subtract_numbers(calculator, result, a, b):
"""Subtract b from a."""
result['value'] = calculator.subtract(a, b)
@when(parsers.parse('I divide {a:d} by {b:d}'))
def divide_numbers(calculator, result, a, b):
"""Divide a by b."""
try:
result['value'] = calculator.divide(a, b)
except ZeroDivisionError as e:
result['error'] = e
@then(parsers.parse('the result is {expected:d}'))
def check_result(result, expected):
"""Verify the calculation result."""
assert result['value'] == expected
@then('I get a division error')
def check_division_error(result):
"""Verify division error occurred."""
assert result['error'] is not None
assert isinstance(result['error'], ZeroDivisionError)
Verification¶
- Run BDD tests:
- Run mutation testing:
Troubleshooting¶
Step definitions are being mutated¶
Exclude the steps directory:
Feature file changes not detected¶
Feature files are not Python code, so they don't trigger mutation testing. Only the source code
(src/) is mutated.
pytest-mock Integration¶
Goal¶
Use pytest-mock alongside pytest-gremlins for mocking external dependencies.
Configuration¶
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-mock>=3.12.0",
"pytest-gremlins>=1.0.0",
]
[tool.pytest-gremlins]
paths = ["src"]
Example Tests with Mocking¶
"""Tests using pytest-mock alongside mutation testing."""
import pytest
class TestEmailService:
"""Tests for email service with mocked SMTP."""
def test_send_email_calls_smtp(self, mocker):
"""Email service calls SMTP client."""
mock_smtp = mocker.patch('myapp.email.SMTPClient')
from myapp.email import EmailService
service = EmailService()
service.send('test@example.com', 'Subject', 'Body')
mock_smtp.return_value.send.assert_called_once()
def test_send_email_includes_recipient(self, mocker):
"""SMTP receives correct recipient."""
mock_smtp = mocker.patch('myapp.email.SMTPClient')
from myapp.email import EmailService
service = EmailService()
service.send('user@example.com', 'Hello', 'World')
call_args = mock_smtp.return_value.send.call_args
assert 'user@example.com' in str(call_args)
def test_send_email_handles_smtp_error(self, mocker):
"""SMTP errors are handled gracefully."""
mock_smtp = mocker.patch('myapp.email.SMTPClient')
mock_smtp.return_value.send.side_effect = ConnectionError('SMTP down')
from myapp.email import EmailService
service = EmailService()
result = service.send('test@example.com', 'Subject', 'Body')
assert result is False
Verification¶
- Tests with mocks pass:
- Mutation testing runs correctly:
Troubleshooting¶
Mocked code is being mutated¶
Only source code is mutated. Mocked behavior in tests isn't affected.
Mutations in mock setup code¶
If you have mock factories in src/, exclude them:
pytest-asyncio Integration¶
Goal¶
Run mutation testing on async code with pytest-asyncio.
Configuration¶
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.23.0",
"pytest-gremlins>=1.0.0",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
[tool.pytest-gremlins]
paths = ["src"]
Example Async Tests¶
"""Tests for async code with mutation testing."""
import pytest
class TestAsyncService:
"""Tests for async service."""
async def test_fetch_data_returns_result(self):
"""Async fetch returns data."""
from myapp.async_service import fetch_data
result = await fetch_data('resource-id')
assert result is not None
assert 'data' in result
async def test_fetch_data_with_invalid_id_raises(self):
"""Invalid ID raises ValueError."""
from myapp.async_service import fetch_data
with pytest.raises(ValueError, match='Invalid resource ID'):
await fetch_data('')
async def test_batch_fetch_returns_all_results(self):
"""Batch fetch returns result for each ID."""
from myapp.async_service import batch_fetch
results = await batch_fetch(['a', 'b', 'c'])
assert len(results) == 3
Verification¶
- Async tests pass:
- Mutation testing works with async code:
Troubleshooting¶
RuntimeError: Event loop is closed¶
Use asyncio_mode = "auto" in pytest config:
Async fixtures not working¶
Ensure fixtures are marked as async:
Multiple Plugins Together¶
Complete Configuration¶
[project]
name = "myproject"
version = "1.0.0"
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.1.0",
"pytest-xdist>=3.5.0",
"pytest-asyncio>=0.23.0",
"pytest-mock>=3.12.0",
"pytest-gremlins>=1.0.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-ra --strict-markers"
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
fail_under = 80
[tool.pytest-gremlins]
paths = ["src"]
exclude = [
"**/test_*",
"**/conftest.py",
"**/__pycache__/*",
]
CI Workflow with All Plugins¶
name: Full Quality Pipeline
jobs:
test:
name: Tests with Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e ".[dev]"
# Parallel tests with coverage
- run: pytest tests/ -n auto --cov=src --cov-report=xml
- uses: codecov/codecov-action@v4
with:
files: coverage.xml
mutation:
name: Mutation Testing
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e ".[dev]"
# Mutation testing (no xdist, uses own parallelism)
- run: pytest --gremlins --gremlin-parallel --gremlin-workers=4 --gremlin-report=html
- uses: actions/upload-artifact@v4
with:
name: mutation-report
path: coverage/gremlins/