Python Test Updater
Update Python tests to work correctly with new code versions.
Workflow
1. Understand the Inputs
Gather required inputs:
- •Old version of Python code (file or content)
- •New version of Python code (file or content)
- •Old test module or test functions (file or content)
Verify inputs:
- •Code files are valid Python
- •Test files use pytest or unittest
- •Files are readable
2. Analyze Code Changes
Automated analysis:
python scripts/analyze_code_diff.py <old_file> <new_file>
Manual analysis:
- •Read both old and new code versions
- •Identify what changed
- •Understand the nature of changes
Identify change types:
Function signature changes:
- •Added parameters
- •Removed parameters
- •Renamed parameters
- •Changed parameter types
- •Changed default values
Return value changes:
- •Changed return type
- •Changed return structure
- •Added return fields
- •Removed return fields
Behavior changes:
- •Modified logic
- •Changed validation rules
- •Updated error handling
- •Altered workflows
Class changes:
- •Added/removed methods
- •Modified constructor
- •Changed inheritance
- •Updated attributes
Async changes:
- •Sync to async conversion
- •Async to sync conversion
See test-update-patterns.md for detailed patterns.
3. Analyze Test Structure
Read the old tests:
- •Identify test functions
- •Understand test setup (fixtures, mocks)
- •Note test assertions
- •Check test organization
Identify test components:
- •Test functions/methods
- •Fixtures
- •Parametrized tests
- •Mocked dependencies
- •Assertions
Map tests to code:
- •Which tests cover which functions/classes
- •What behavior each test verifies
- •What assertions check what conditions
4. Determine Required Updates
For each code change, identify test impact:
Signature changes → Update function calls
# Old code: function(arg1, arg2) # New code: function(arg1, arg2, arg3=default) # Old test result = function(value1, value2) # Updated test result = function(value1, value2) # Works with default # OR result = function(value1, value2, value3) # Explicit value
Return value changes → Update assertions
# Old code: return value
# New code: return {"result": value, "status": "ok"}
# Old test
assert result == expected_value
# Updated test
assert result["result"] == expected_value
assert result["status"] == "ok"
Behavior changes → Update expected values
# Old code: validates length >= 6
# New code: validates length >= 8 and has digit
# Old test
assert validate("abc123") == True
# Updated test
assert validate("abc12345") == True # Updated
assert validate("abc123") == False # Now fails
New functionality → Add new tests
# New code: added get_display_name() method
# Add new test
def test_get_display_name():
obj = MyClass("value")
assert obj.get_display_name() == "Value: value"
5. Update Test Code
Apply updates systematically:
Step 1: Update imports if needed
# If new exceptions or classes added from module import NewException, NewClass
Step 2: Update function/method calls
- •Add new required parameters
- •Remove obsolete parameters
- •Rename parameters if changed
- •Update keyword arguments
Step 3: Update assertions
- •Change expected values if behavior changed
- •Update assertion structure if return type changed
- •Add new assertions for new fields
- •Remove assertions for removed fields
Step 4: Update exception handling
# Old
with pytest.raises(OldException):
function()
# New
with pytest.raises(NewException):
function()
Step 5: Update async/await if needed
# Old
def test_function():
result = function()
# New (if function became async)
@pytest.mark.asyncio
async def test_function():
result = await function()
Step 6: Add new test cases
- •Test new functionality
- •Test new parameters
- •Test new behavior
- •Test edge cases
6. Run Tests
Execute the updated tests:
# Run all tests pytest test_file.py # Run specific test pytest test_file.py::test_function # Run with verbose output pytest -v test_file.py
Check results:
- •All tests should pass
- •No import errors
- •No syntax errors
- •No assertion failures
7. Fix Remaining Failures
If tests still fail:
Analyze error messages:
- •Read the error carefully
- •Identify what's failing
- •Understand why it's failing
Common failure types:
1. AssertionError
AssertionError: assert 10 == 15
→ Expected value changed, update assertion
2. TypeError
TypeError: function() missing 1 required positional argument: 'new_param'
→ Add missing parameter to function call
3. AttributeError
AttributeError: 'dict' object has no attribute 'field'
→ Return type changed, update how result is accessed
4. ImportError
ImportError: cannot import name 'OldClass'
→ Class renamed or removed, update import
Fix each failure:
- •Identify the root cause
- •Apply appropriate fix
- •Re-run tests
- •Verify fix works
8. Verify and Refine
Final verification:
- •All tests pass
- •Test coverage maintained
- •Test intent preserved
- •Code quality good
Refine if needed:
- •Improve test names
- •Add docstrings
- •Use fixtures for common setup
- •Parametrize similar tests
Example refinement:
# Before
def test_function_case1():
assert function(5) == 10
def test_function_case2():
assert function(10) == 20
# After (parametrized)
@pytest.mark.parametrize("input,expected", [
(5, 10),
(10, 20),
])
def test_function(input, expected):
assert function(input) == expected
Common Update Patterns
Pattern 1: Add Parameter with Default
Code change:
# Old
def function(a, b):
return a + b
# New
def function(a, b, c=0):
return a + b + c
Test update:
# Old test (still works)
def test_function():
assert function(1, 2) == 3
# Add new test for new parameter
def test_function_with_c():
assert function(1, 2, 3) == 6
Pattern 2: Change Return Type
Code change:
# Old
def get_data():
return [1, 2, 3]
# New
def get_data():
return {"data": [1, 2, 3], "count": 3}
Test update:
# Old
def test_get_data():
data = get_data()
assert len(data) == 3
# New
def test_get_data():
result = get_data()
assert len(result["data"]) == 3
assert result["count"] == 3
Pattern 3: Change Validation Logic
Code change:
# Old
def validate(value):
return len(value) >= 6
# New
def validate(value):
return len(value) >= 8 and any(c.isdigit() for c in value)
Test update:
# Old
def test_validate():
assert validate("abc123") == True
assert validate("abc") == False
# New
def test_validate():
assert validate("abc12345") == True # Updated
assert validate("abc123") == False # Now fails validation
assert validate("abcdefgh") == False # No digit
assert validate("abc") == False
Pattern 4: Sync to Async
Code change:
# Old
def fetch():
return data
# New
async def fetch():
return await async_data()
Test update:
# Old
def test_fetch():
result = fetch()
assert result is not None
# New
@pytest.mark.asyncio
async def test_fetch():
result = await fetch()
assert result is not None
Best Practices
Preserve Test Intent
- •Keep testing the same functionality
- •Don't change what's being verified
- •Only update how it's tested
Maintain Coverage
- •Don't remove tests unless functionality removed
- •Add tests for new functionality
- •Keep edge case tests
Update Systematically
- •Fix one type of issue at a time
- •Run tests after each change
- •Verify fixes don't break other tests
Improve While Updating
- •Use fixtures for common setup
- •Parametrize similar tests
- •Improve test names and docs
Verify Thoroughly
- •Run full test suite
- •Check for flaky tests
- •Verify test independence
Troubleshooting
Issue: Tests pass but don't test new behavior
Solution:
- •Add new test cases for new functionality
- •Update existing tests to cover new parameters
- •Verify test coverage
Issue: Can't determine what changed
Solution:
- •Use code diff analyzer script
- •Compare function signatures manually
- •Run old tests against new code to see failures
- •Analyze error messages
Issue: Too many test failures
Solution:
- •Fix one test at a time
- •Group similar failures
- •Fix systematic issues first (imports, signatures)
- •Then fix assertion issues
Issue: Tests pass but behavior seems wrong
Solution:
- •Verify test assertions are correct
- •Check if test is actually testing new behavior
- •Add more specific assertions
- •Test edge cases
Example Workflow
Scenario: Function signature changed
Old code:
def calculate_price(quantity, unit_price):
return quantity * unit_price
New code:
def calculate_price(quantity, unit_price, discount=0.0):
subtotal = quantity * unit_price
return subtotal * (1 - discount)
Old test:
def test_calculate_price():
price = calculate_price(5, 10.0)
assert price == 50.0
Analysis:
- •Added parameter:
discountwith default value 0.0 - •Behavior unchanged when discount not provided
- •New behavior when discount provided
Updated test:
def test_calculate_price():
# Test without discount (original behavior)
price = calculate_price(5, 10.0)
assert price == 50.0
def test_calculate_price_with_discount():
# Test with discount (new behavior)
price = calculate_price(5, 10.0, 0.1)
assert price == 45.0 # 50 * 0.9
Verification:
pytest test_file.py -v # Both tests should pass
Output Format
Provide updated test code with:
- •
Summary of changes:
- •What was updated
- •Why it was updated
- •New tests added
- •
Updated test code:
- •Complete updated test file
- •All necessary imports
- •All test functions
- •
Verification notes:
- •How to run tests
- •Expected results
- •Any caveats or notes