Testing Deprecations¶
This guide covers how to effectively test deprecation warnings in your code.
Basic Testing with pytest¶
Testing That Warnings Are Emitted¶
import pytest
from mypackage._deprecations import OLD_API_DEPRECATION
from mypackage import old_api
def test_deprecated_function_warns():
"""Test that deprecated function emits warning."""
with pytest.warns(type(OLD_API_DEPRECATION)):
result = old_api("test data")
assert result == expected_result
Testing Warning Messages¶
def test_deprecation_message():
"""Test that deprecation has correct message."""
with pytest.warns(type(OLD_API_DEPRECATION)) as warning_info:
old_api()
# Check the warning message
assert len(warning_info) == 1
assert "use new_api instead" in str(warning_info[0].message)
Testing That New APIs Don't Warn¶
def test_new_api_no_warning():
"""Ensure new API doesn't emit deprecation warnings."""
with warnings.catch_warnings():
warnings.simplefilter("error") # Turn warnings into errors
result = new_api("test data") # Should not raise
assert result == expected_result
Testing Different Warning Categories¶
Deprecator automatically changes warning categories based on version:
from packaging.version import Version
from deprecator import for_package
def test_warning_categories():
"""Test that deprecations have correct categories based on version."""
# Test with different versions
early_deprecator = for_package("mypackage", Version("1.0.0"))
active_deprecator = for_package("mypackage", Version("2.0.0"))
expired_deprecator = for_package("mypackage", Version("3.0.0"))
# Same definition, different warning types based on version
early_warning = early_deprecator.define(
"Test warning", warn_in="2.0.0", gone_in="3.0.0"
)
active_warning = active_deprecator.define(
"Test warning", warn_in="2.0.0", gone_in="3.0.0"
)
expired_warning = expired_deprecator.define(
"Test warning", warn_in="2.0.0", gone_in="3.0.0"
)
# Check warning types
assert "PendingDeprecationWarning" in str(type(early_warning))
assert "DeprecationWarning" in str(type(active_warning))
assert "ExpiredDeprecationWarning" in str(type(expired_warning))
Testing Migration Paths¶
Ensure that old and new APIs produce equivalent results:
class TestMigrationPath:
def test_old_new_equivalence(self):
"""Ensure old and new APIs produce same results."""
test_data = "sample data"
# Old API (with warning)
with pytest.warns(DeprecationWarning):
old_result = old_api(test_data)
# New API (no warning)
new_result = new_api(test_data)
assert old_result == new_result
def test_parameter_migration(self):
"""Test that old parameters map correctly to new ones."""
# Old parameter name (with warning)
with pytest.warns(DeprecationWarning):
old_result = process(old_param="value")
# New parameter name (no warning)
new_result = process(new_param="value")
assert old_result == new_result
Using pytest Fixtures¶
Create reusable fixtures for testing deprecations:
# conftest.py
import pytest
from packaging.version import Version
from deprecator import for_package
@pytest.fixture
def test_deprecator():
"""Deprecator with controllable version for testing."""
return for_package("test-package", Version("1.0.0"))
@pytest.fixture
def sample_deprecation(test_deprecator):
"""Sample deprecation for testing."""
return test_deprecator.define(
"Test feature is deprecated",
warn_in="0.5.0",
gone_in="2.0.0"
)
# test_deprecations.py
def test_with_fixture(sample_deprecation):
"""Test using deprecation fixture."""
with pytest.warns(type(sample_deprecation)):
sample_deprecation.warn()
Testing with Different Python Warning Filters¶
import warnings
def test_warning_filters():
"""Test deprecations with different warning filters."""
# Test with warnings as errors
with warnings.catch_warnings():
warnings.filterwarnings("error", category=DeprecationWarning)
with pytest.raises(DeprecationWarning):
old_api()
# Test with warnings ignored
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
# Should not produce any warnings
with pytest.warns(None) as warning_list:
old_api()
# Note: The warning is still generated but filtered
Testing Decorators¶
Test that the deprecation decorator works correctly:
def test_decorator_application():
"""Test that @deprecation.apply works correctly."""
from mypackage._deprecations import OLD_FUNCTION_DEPRECATION
@OLD_FUNCTION_DEPRECATION.apply
def decorated_function():
return "result"
# Function should still work but emit warning
with pytest.warns(type(OLD_FUNCTION_DEPRECATION)):
result = decorated_function()
assert result == "result"
Testing Stack Levels¶
Ensure warnings point to the correct location:
def test_warning_stacklevel():
"""Test that warnings have correct stack level."""
with pytest.warns(DeprecationWarning) as warning_info:
# This line should be reported as the warning location
old_api()
# Check that the warning points to this test file, not internal code
assert __file__ in str(warning_info[0].filename)
Integration with pytest Plugin¶
The deprecator pytest plugin provides additional functionality:
# Run tests with deprecation checking
# pytest --deprecator-error # Fail on expired deprecations
# pytest --deprecator-github-annotations # GitHub CI annotations
def test_with_plugin(pytester):
"""Test using deprecator pytest plugin."""
pytester.makepyfile("""
from deprecator import for_package
def test_expired():
deprecator = for_package("test", "3.0.0")
warning = deprecator.define(
"Expired", warn_in="1.0.0", gone_in="2.0.0"
)
warning.warn() # This is expired at version 3.0.0
""")
# Should fail with --deprecator-error
result = pytester.runpytest("--deprecator-error")
result.assert_outcomes(failed=1)
Testing CLI Commands¶
Test the deprecator CLI tools:
import subprocess
def test_cli_validate():
"""Test deprecator CLI validation."""
result = subprocess.run(
["deprecator", "validate-package", "mypackage"],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "validation passed" in result.stdout.lower()
def test_cli_show_registry():
"""Test showing deprecation registry."""
result = subprocess.run(
["deprecator", "show-registry"],
capture_output=True,
text=True
)
assert result.returncode == 0
# Check that your deprecations appear in output
Continuous Integration Testing¶
GitHub Actions Example¶
name: Test Deprecations
on: [push, pull_request]
jobs:
test-deprecations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -e .[test,cli]
- name: Run deprecation tests
run: |
# Run with deprecation error checking
pytest tests/ --deprecator-error
- name: Validate package deprecations
run: |
deprecator validate-package mypackage
- name: Check for expired deprecations
run: |
deprecator show-registry
# Fail if expired deprecations exist
! deprecator show-registry | grep -q "expired"
Best Practices¶
- Test both paths: Always test both deprecated and new code paths
- Check messages: Verify deprecation messages are clear and helpful
- Test categories: Ensure warnings have correct categories for your version
- Use fixtures: Create reusable test fixtures for common deprecations
- Test migration: Verify old and new APIs produce equivalent results
- CI integration: Run deprecation tests in your CI pipeline
- Test removal: Have tests ready to verify code removal at
gone_inversion
Common Testing Patterns¶
Pattern: Version-Specific Tests¶
import pytest
from packaging.version import Version
from mypackage import __version__
@pytest.mark.skipif(
Version(__version__) >= Version("2.0.0"),
reason="Old API removed in 2.0.0"
)
def test_old_api_still_works():
"""Test old API while it still exists."""
with pytest.warns(DeprecationWarning):
result = old_api()
assert result is not None
@pytest.mark.skipif(
Version(__version__) < Version("2.0.0"),
reason="Old API not yet removed"
)
def test_old_api_removed():
"""Test that old API is properly removed."""
with pytest.raises(AttributeError):
from mypackage import old_api
Pattern: Parametrized Warning Tests¶
@pytest.mark.parametrize("func,should_warn", [
(old_api, True),
(new_api, False),
(legacy_function, True),
(modern_function, False),
])
def test_deprecation_warnings(func, should_warn):
"""Test that only deprecated functions emit warnings."""
if should_warn:
with pytest.warns(DeprecationWarning):
func()
else:
with warnings.catch_warnings():
warnings.simplefilter("error")
func() # Should not raise