Using Code Coverage as a Check for Test Refactoring

TLDR: Use code coverage reports to verify that test refactorings haven't accidentally changed what functionality you're testing. Same coverage percentage before and after refactoring gives confidence your tests still cover the same code paths.
If we refactor code, tests should cover us from breaking things. But what about refactoring tests? How do we know our tests are still testing the same functionality as before?
While test refactoring isn't widely discussed I found myself refactoring the tests of my applications whilst reviewing and researching testing approaches.
Why am I refactoring tests?
Experimentation with executable specifications
I often use Fluent Test DSLs to develop executable specifications. The DSL may change if it expresses the functionality more clearly or concisely. This might involve renaming a method, changing an assertion, or refactoring the DSL implementation. These changes are usually low risk, but it's helpful to verify we haven't accidentally missed any behaviour.
Optimise assertions
Sometimes a single test ends up testing too much and you start trimming the test to only validate the exact behaviour you intended to test, possibly aiming for a single clean assert
. This sometimes means creating multiple tests to cover the same functionality.
The opposite may also happen. You have too many tests that affect feedback time and you want to combine tests to improve performance. Not ideal but may be a pragmatic approach at times.
Consistent test naming
Test naming can grow organically and become inconsistent over time. This happens especially when tests use sentence descriptions. Small changes pile up until your tests all follow slightly different naming styles.
You might adopt Testing naming guidelines for consistency. When you start applying them, you realise you haven't reviewed all tests together recently because you've been adding features one by one instead.
During this naming cleanup, you often find other small improvements you want to make. Some tests may no longer seem relevant in the wider context. This leads to more small refactorings.
AI-supported test refactoring
You might want to use AI to help with these refactorings.
Take the consistent test naming example above. I use AI agents for this task, asking them to review my current tests and align with testing guidelines. I also ask the AI to check for small cleanup opportunities.
An AI assistant can make a plan and list all the changes I need to implement. I then work through them one by one, often by copy-pasting. This isn't the most exciting work, so it would be easier if the AI could make these changes directly after I approve them.
This raises a question: how do I know I'm still testing the same functionality without checking every line of code?
Test renaming shouldn't change the test logic, but splitting or trimming tests might. You might even let an AI agent change code directly, which could lead to unintended changes.
Verifying test refactorings with code coverage
The first check after any refactoring is obvious: do the tests still compile and run?
But how do we know the tests still cover the same functionality after refactoring?
I use code coverage as a basic check for this. Code coverage shows which code lines your tests execute, and it can catch obvious mistakes during test refactoring. If I have exactly the same coverage percentage before and after refactoring (like 94.614%
), it suggests I haven't accidentally deleted entire tests or changed which code paths are exercised.
This isn't code coverage's intended purpose, and it has clear limitations - it won't catch changes in test logic or assertions that still execute the same code lines. But it's a useful test for catching the most obvious refactoring mistakes.
When to refactor tests
I wanted to include the types of test refactorings that code coverage can help verify. These fall into two categories: individual test refactorings are usually relatively easy to manage, but sometimes we need to, or should, refactor tests at the level of entire test suites.
Refactor individual tests when:
The test is too complex or difficult to understand
The test asserts too many behaviours at once
The test is too slow or resource-intensive
The test doesn't match the rest of the test suite style
The test is outdated or no longer relevant
Refactor entire test suites when:
The test suite fails with almost every production code change
The test suite lacks consistency across tests
The test suite doesn't align with current testing guidelines
The test suite has become too large or complex to manage
The test framework or design needs upgrading
Conclusion
Using code coverage as a basic check for test refactorings isn't comprehensive, but it's a simple technique that catches obvious mistakes.
I'm curious how others approach this challenge. Do you have other methods for ensuring your tests haven't been unintentionally modified during refactoring?
Thank you for reading, Hans
Subscribe to my newsletter
Read articles from Hans L'Hoest directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Hans L'Hoest
Hans L'Hoest
I write about: Software architecture and engineering, Better software better. DDD, Scala and Rust