Today, we’ll discuss an interesting question that I hear quite often: how to refactor production code given that you don’t have adequate test coverage. I don’t think I addressed it explicitly in the past, so I’m fixing it with this post.
1. Refactoring and test coverage
We all sometimes find ourselves working on legacy projects. In fact, legacy projects are more prevalent than green-field development, simply because all green-field projects eventually join the legacy cohort.
Legacy projects are notorious for their code quality, both production and test code. Some projects may even have no tests at all.
How do you refactor such a project?
In order to refactor safely, you must have tests, to ensure that your refactoring doesn’t break anything.
But how do you introduce tests in a project with tightly coupled, untestable code? If the code was good and testable, you wouldn’t need to refactor it in the first place.
This is the chicken-and-egg problem of legacy projects:
You need tests to ensure the refactoring is successful.
But before writing tests, you need to refactor the underlying production code to make it testable.
There’s no way around this issue: test and production code are intrinsically connected; it’s impossible to create good tests without putting effort into the code base they cover.
How can you possibly reconcile these two circular requirements?
2. Working around the Test Pyramid
To overcome this issue, you have to start with end-to-end and integration tests first.
End-to-end tests are tests that fully emulate the end user. For example, if your application is a website, end-to-end tests would verify it using its web interface.
Normally, end-to-end tests are the minority and should only be applied to the most critical functionality — features in which you don’t ever want to see any bugs — and only when you can’t get the same degree of protection with unit or integration tests.
Most of the work should normally be done by unit tests, because they are cheaper to write and maintain.
In a legacy project, though, end-to-end tests are the only option available because they are the only type of tests that doesn’t require changes in the underlying production code (since they interact with the application using the same interface as regular users).
End-to-end tests also have the highest resilience to refactoring and will not raise false alarms because of your refactoring activities.
In some legacy projects, there’s also a middle ground where you can write integration tests instead of end-to-end tests. Those are projects that have at least some degree of decoupling.
A good example is an ASP.NET API application with messy code that still has controllers, despite all the spaghettiness. You can test those controllers instead of the fully-fledged API running in a separate process. The resulting tests are going to be integration tests and will be easier to maintain than end-to-end tests.
The good news is that the large number of end-to-end and integration tests is a temporary situation. Once you start refactoring, push your tests as far down the test pyramid as you can: replace end-to-end tests with integration tests and integration tests — with unit tests.
And so the path to refactoring of legacy code bases is this:
Identify a small, cohesive area in your application that could be refactored relatively quickly and without raising havoc to the rest of the code base.
Cover this area with end-to-end or integration tests.
Refactor it by separating business logic and orchestration responsibilities (the Humble Object design pattern).
Cover the business logic with unit tests; orchestration — with integration tests.
Delete end-to-end tests (given they don’t provide extra test coverage).
Remember, adequate unit test coverage is impossible without quality production code base.
For more on the Humble Object pattern and how exactly to do the refactoring, check out my Unit Testing book if you haven’t already.
Enjoy this message? Here are more things you might like:
Workshops — I offer a 2-day workshop for organizations on Domain-Driven Design and Unit Testing. Reply to this email to discuss.
Unit Testing Principles, Patterns and Practices — A book for people who already have some experience with unit testing and want to bring their skills to the next level.
Learn more »
My Pluralsight courses — The topics include Unit Testing, Domain-Driven Design, and more.
Learn more »