| name | java-test-updater |
| description | Update Java test classes and methods to work with new code versions after refactoring or modifications. Use when code changes break existing tests due to signature changes, refactoring, or behavior modifications. Takes old and new code versions plus old tests as input, and outputs updated tests that compile and pass against the new code. Handles method signature changes, class refactoring, assertion updates, and mock modifications. |
Java Test Updater
Overview
Automatically update Java test code to align with changes in production code, ensuring tests compile and pass after refactoring, signature changes, or behavior modifications.
Workflow
1. Analyze Code Changes
Compare old and new versions of the production code to identify changes:
Read both versions:
- Old code version (before changes)
- New code version (after changes)
- Identify the specific class and methods that changed
Categorize changes:
- Signature changes: Parameter types, parameter order, return type, method name
- Refactoring: Class renamed, method moved, package changed
- Behavior changes: Logic modified, new exceptions, different return values
- API changes: New methods added, old methods removed
Example change analysis:
public double calculateDiscount(double price) {
return price * 0.1;
}
public double calculateDiscount(double price, double discountRate) {
if (discountRate < 0 || discountRate > 1) {
throw new IllegalArgumentException("Invalid discount rate");
}
return price * discountRate;
}
Identified changes:
- Added parameter:
discountRate
- Added validation with exception
- Changed calculation logic
2. Analyze Existing Tests
Read and understand the old test code:
Identify test structure:
- Test class name and package
- Test methods and their purposes
- Setup and teardown methods (
@Before, @BeforeEach, @After, @AfterEach)
- Test data and fixtures
- Mocks and stubs used
Map tests to code changes:
- Which tests call the changed methods
- Which assertions depend on changed behavior
- Which mocks need updating
Example old test:
@Test
public void testCalculateDiscount() {
double price = 100.0;
double result = calculator.calculateDiscount(price);
assertEquals(10.0, result, 0.01);
}
3. Update Method Calls
Modify test code to match new method signatures:
For added parameters:
calculator.calculateDiscount(price)
calculator.calculateDiscount(price, 0.1)
For removed parameters:
calculator.calculateDiscount(price, taxRate, shippingCost)
calculator.calculateDiscount(price, shippingCost)
For parameter type changes:
calculator.process(userId)
calculator.process(Long.valueOf(userId))
For renamed methods:
calculator.computeTotal()
calculator.calculateTotal()
For moved methods:
calculator.validateInput(data)
validator.validateInput(data)
4. Update Assertions
Adjust test assertions to match new behavior:
For changed return values:
assertEquals(10.0, result, 0.01);
assertEquals(8.0, calculator.calculateDiscount(100.0, 0.08), 0.01);
For new exceptions:
@Test
public void testInvalidDiscountRate() {
assertThrows(IllegalArgumentException.class, () -> {
calculator.calculateDiscount(100.0, -0.1);
});
}
For changed behavior:
assertTrue(result.isEmpty());
assertNull(result);
For modified object state:
assertEquals(1, cart.getItemCount());
assertEquals(1, cart.getItems().size());
5. Update Mocks and Stubs
Modify mock objects to match new interfaces:
For Mockito mocks with signature changes:
when(repository.findById(userId)).thenReturn(user);
when(repository.findById(userId)).thenReturn(Optional.of(user));
For added parameters in mocked methods:
when(service.process(data)).thenReturn(result);
when(service.process(data, context)).thenReturn(result);
when(service.process(eq(data), any(Context.class))).thenReturn(result);
For changed mock behavior:
when(validator.isValid(input)).thenReturn(true);
doNothing().when(validator).validate(input);
doThrow(new ValidationException()).when(validator).validate(invalidInput);
6. Add New Test Cases
Create additional tests for new functionality or edge cases:
For new parameters:
@Test
public void testCalculateDiscountWithZeroRate() {
double result = calculator.calculateDiscount(100.0, 0.0);
assertEquals(0.0, result, 0.01);
}
@Test
public void testCalculateDiscountWithMaxRate() {
double result = calculator.calculateDiscount(100.0, 1.0);
assertEquals(100.0, result, 0.01);
}
For new exceptions:
@Test
public void testNegativeDiscountRate() {
assertThrows(IllegalArgumentException.class, () -> {
calculator.calculateDiscount(100.0, -0.5);
});
}
@Test
public void testDiscountRateAboveOne() {
assertThrows(IllegalArgumentException.class, () -> {
calculator.calculateDiscount(100.0, 1.5);
});
}
For new behavior:
@Test
public void testNewFeature() {
Result result = service.newMethod(input);
assertNotNull(result);
assertEquals(expectedValue, result.getValue());
}
7. Update Test Setup and Teardown
Modify test fixtures if dependencies changed:
For constructor changes:
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@BeforeEach
void setUp() {
validator = new Validator();
calculator = new Calculator(validator);
}
For new dependencies:
@Mock
private Repository repository;
@Mock
private Repository repository;
@Mock
private CacheService cacheService;
@BeforeEach
void setUp() {
service = new Service(repository, cacheService);
}
8. Verify Compilation
Ensure updated tests compile without errors:
Check for compilation issues:
- Import statements are correct
- All types are resolved
- Method signatures match
- No syntax errors
Common compilation fixes:
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
Long userId = 123L;
List<String> items = new ArrayList<>();
Compile tests:
mvn test-compile
./gradlew testClasses
9. Run and Verify Tests
Execute tests to ensure they pass:
Run all updated tests:
mvn test -Dtest=CalculatorTest
./gradlew test --tests CalculatorTest
Analyze test results:
- All tests should pass (green)
- No test failures
- No test errors
- Check test output for warnings
If tests fail:
- Read failure message and stack trace
- Identify the cause (assertion failure, exception, etc.)
- Fix the test code or identify issues in production code
- Re-run tests
- Repeat until all tests pass
10. Validate Test Quality
Ensure updated tests maintain quality standards:
Check test coverage:
- New code paths are tested
- Edge cases are covered
- Exception handling is tested
Verify test independence:
- Tests don't depend on execution order
- Each test can run in isolation
- No shared mutable state between tests
Review test clarity:
- Test names are descriptive
- Test logic is clear
- Assertions are meaningful
Update Patterns
Pattern 1: Signature Change with Added Parameter
Old code:
public String formatName(String firstName, String lastName) {
return firstName + " " + lastName;
}
New code:
public String formatName(String firstName, String lastName, String title) {
return title + " " + firstName + " " + lastName;
}
Old test:
@Test
public void testFormatName() {
String result = formatter.formatName("John", "Doe");
assertEquals("John Doe", result);
}
Updated test:
@Test
public void testFormatName() {
String result = formatter.formatName("John", "Doe", "Mr.");
assertEquals("Mr. John Doe", result);
}
@Test
public void testFormatNameWithEmptyTitle() {
String result = formatter.formatName("John", "Doe", "");
assertEquals(" John Doe", result);
}
Pattern 2: Method Refactored to Different Class
Old code:
public class UserService {
public boolean validateEmail(String email) {
return email.contains("@");
}
}
New code:
public class UserService {
private EmailValidator validator;
}
public class EmailValidator {
public boolean isValid(String email) {
return email.contains("@");
}
}
Old test:
@Test
public void testValidateEmail() {
assertTrue(userService.validateEmail("user@example.com"));
}
Updated test:
@Mock
private EmailValidator emailValidator;
@BeforeEach
void setUp() {
userService = new UserService(emailValidator);
}
@Test
public void testEmailValidation() {
when(emailValidator.isValid("user@example.com")).thenReturn(true);
}
@Test
public void testEmailValidatorIsValid() {
EmailValidator validator = new EmailValidator();
assertTrue(validator.isValid("user@example.com"));
}
Pattern 3: Return Type Changed
Old code:
public User findUser(Long id) {
return repository.findById(id);
}
New code:
public Optional<User> findUser(Long id) {
return repository.findById(id);
}
Old test:
@Test
public void testFindUser() {
User user = service.findUser(123L);
assertNotNull(user);
assertEquals("John", user.getName());
}
Updated test:
@Test
public void testFindUser() {
Optional<User> userOpt = service.findUser(123L);
assertTrue(userOpt.isPresent());
assertEquals("John", userOpt.get().getName());
}
@Test
public void testFindUserNotFound() {
Optional<User> userOpt = service.findUser(999L);
assertFalse(userOpt.isPresent());
}
Tips
- Always read both old and new code versions completely
- Identify all affected test methods before making changes
- Update one test method at a time
- Compile frequently to catch errors early
- Run tests after each update to verify correctness
- Maintain test coverage - don't remove tests without replacement
- Add tests for new edge cases introduced by changes
- Keep test names descriptive and up-to-date
- Update test documentation/comments if behavior changed
- Use IDE refactoring tools when available for safe renames