Removing a Feature is a Change in Behavior
How do you remove functionality from an application using TDD?
On my Discord, @Oye recently asked about removing a feature from a codebase using Test-Driven Development (TDD). I love this question, because it hits the heart of one of the misunderstandings about that I see:
TDD is only good for adding features by adding new, failing tests and adding code to make them pass.
In fact, TDD is great for making any behavior change in your code. Adding and removing behavior are all changes.
If you’ve read my article on Clarifying the Goal of Behavior Change, you’ll see that the first thing I do when starting my TDD process is define:
What should the application do?
How will you know it did it?
When removing behavior, you can negate these statements:
What does it currently do that it should stop doing?
How will you know it’s no longer doing it?
But really, it’s the same thing!
For example, if your application currently shows an option to “ship today”, but the business says that’s too expensive (and unlikely to be solved soon), it needs to go. The next step is to answer those two questions:
It should stop showing the “Ship Today” option.
The test against the UI or API should ensure “Ship Today” doesn’t show up.
If you TDD’d the original implementation, then there should be a test that was written when the “Ship Today” option was first added. Go into that test, and instead of asserting that it is there, assert that it’s not there.
If you didn’t originally TDD it, or you never wrote a test for that behavior, now’s the time to do so. Write a test that looks at all the options for shipping and asserts that “Ship Today” is not there.
To get that (now) failing test to pass, you’ll likely delete code. Propagate those deletions until everything is passing. It’ll also mean you’ll change other tests as you follow the code across different objects and layers. The goal is to stay out of the situation where a lot of tests are failing, and it’s taking a long time to delete/change the code to get them all passing.
As you finish removing the feature, you may find that the “negative” tests are all that’s left of the feature. You can decide to remove those (treat them as scaffolding), or, if there’s risk that the behavior might resurface in some way, then leave them. Or, it could be left in the test code as a useful trail of documentation.
Sometimes you may find that entire objects are no longer needed, depending on how large the feature is. And then, of course, entire test classes for those objects can also be deleted. As @Suigi says in the Discord, if an entire endpoint is going away, then remove it—and the test that then fails—and look for unused code. Delete that, and “cascade” that delete until all unused code (and tests) is removed.
Again, there’s little difference in the TDD process for removing, because removing something is a change in behavior as much as adding something. Your tests define the behavior you want, then you change the code to make that test pass. Whether you add code or delete code, all that matters is getting that test to pass.