Tales Of TDD: The Big RefactoringFebruary 2, 2022
I’ve heard that you’ve been working on this new feature for the payroll system based on the newly voted government legislation?
Yes, I am.
How is it going? Seems like a major change.
Well, I’m glad you asked. I’ve refactored a large part of the domain based on some new insights I’ve had. This morning I’ve pushed all the code to the git repository. It took me almost two weeks to make the necessary changes.
However, I could use some help though.
Sure thing. How can I help?
You see, in order to refactor the code base I commented out most of the tests. I just hate it when they get in the way. Anyway, can you fix these tests? There shouldn’t be more than 350 of them. That way I’m able to start working on the next user story.
Uhm, … that’s … uhm … interesting.
A couple of things come to mind.
OK. Tell me.
For starters, you mentioned the term refactoring a couple of times. I’m interested in the process that you call refactoring.
Yes. How did you take on this endeavour?
Well, I did what I always do. I started out by changing a bunch of existing methods on several classes, adding and removing parameters, moving code around, etc. … I’ve also added a couple of new classes. Just the usual stuff.
How soon did you execute the tests?
After a couple of hours I wanted to compile the source code. Then I noticed that a bunch of the tests didn’t compile anymore. There were just too many, so I’ve put them in comments. You know, this bothers me the most about tests. They prevent me from refactoring the code. Maybe we should just get rid of these unit tests. We still have the acceptance tests, right?
So you didn’t run the tests?
No. They didn’t even compile.
Did you execute the acceptance tests?
Are you kidding me? They take more than two hours to run. I don’t have time for that. Besides, that’s why we pay the QA folks. It’s their job to make sure that the acceptance tests run smoothly. They’re also a mess. I won’t go anywhere near those.
In summary, you’ve been “restructuring” code for two weeks without executing any kind of automated tests?
I’m afraid so. I could have done a better job on that department, which reminds me to try running the application later on to see if it still works. But I’m curious, you just said “restructuring the code”. Isn’t that the same as refactoring?
Refactoring is a form of restructuring, but they aren’t necessarily the same thing. Refactoring is making small adjustments to the code without altering its observable behaviour. By adding up all these small changes you basically end up with a bigger change.
I don’t get it. That’s exactly what I’ve been doing. It’s not that I changed thousands of lines of code at once.
There are some very important aspects that I didn’t mention yet.
Ok. What are those aspects then?
The most important aspect of all when it comes to refactoring is to execute the tests after each and every change, no matter how small. That way you know that the system still works as expected. Whenever a test fails, you also know exactly where to find and fix the issue.
I’ve tried that once while doing a code kata with a bunch of people. It made me feel ridiculous. I mean, the code changes became so small that it was almost impossible to mess them up.
That’s what you want, right?
I don’t know. Maybe. But who has time for such things?
You just mentioned that you want to try running the application to see if most of the features still work, right?
What are you going to do when you encounter something that doesn’t work?
In that case, I’ll try to find the code for that broken feature, set out some breakpoints and start debugging to see what’s going on.
Do you have time for that?
Uhm, … I guess so.
More often than not, a debugging session can easily waste a lot of time. Alternatively, by enabling a tight feedback loop we keep ourselves out of such a mess.
Suppose that I only make small changes as you suggest, and I’m unable to find the issue whenever a test fails. Then I still have to debug the code right?
Another important aspect is to also commit the code changes after each cycle. That way you’ll be able to revert to the last good commit and start over, only now using even smaller steps.
So you’re saying that besides making only tiny changes, I have to commit those changes as well? Isn’t that a lot of overhead?
Not if you want to have a fast feedback loop in place.
Who ever said that I want to have a feedback loop?
Well, didn’t you just mention that you wanted to move on to the next user story?
Why’s that? You didn’t even finish your current user story.
I just want to work on something else. I’m kind of fed up with the current user story.
You could have moved on if you’d only made small refactorings, ensured that all the tests still passed and committed the code after each short cycle.
I don’t understand.
Well, if you’d rigorously followed this short feedback cycle that I’ve been describing, then the code would always be in a releasable state, wouldn’t it?
Yeah, maybe that’s true.
Be quick but don’t hurry.
You always say things like that. I still have one question though.
What if I don’t want to refactor a particular part of the code? What if I just want to replace it with some entirely new implementation? Surely I can’t fall back to having a short feedback cycle in that case, right?
When you want to make changes on a larger scale, you can use the “Branch by Abstraction” technique.
What’s that all about?
Going back to your specific case, you develop the new part of the domain just as you would implement it from scratch. This way you can flesh out the design of the new code using Test-Driven Development alongside the existing implementation. When this is finished, you can gradually replace the old implementation with the new implementation at the “seams”.
What’s a seam? I never heard about this in the context of software development before.
This is a term that Michael Feathers uses in his excellent book Working Effectively with Legacy Code . According to his definition, a seam is a place where you can alter behavior in your program without editing in that place. This involves that some kind of abstraction layer is in place that is used by the client code. This abstraction layer enables you to swap out the old implementation with the new implementation.
That way I can work on the new implementation using short feedback cycles. I’ll be able to commit the code without affecting the existing implementation, while also keeping the application in a deployable state.
I’ll have to try that some time for one of the next user stories.
Let me know how that works out.
I will. Now, about those 350 failing tests …
If you and your team want to learn more about how to write maintainable unit tests and get the most out of TDD practices, make sure to have look at our trainings and workshops or check out the books section. Feel free to reach out at firstname.lastname@example.org.
Jan Van Ryswyck
Thank you for visiting my blog. I’m a professional software developer since Y2K. A blogger since Y2K+5. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Past organizer of the European Virtual ALT.NET meetings. Thinking and learning about all kinds of technologies since forever.
Watch The Videos
June 15, 2022
February 2, 2022
December 15, 2021
October 7, 2021
June 22, 2021
- Behavior-Driven Development
- Concurrent Programming
- Continuous Integration
- Core Skills
- Design Patterns
- Domain-Driven Design
- Event Sourcing
- Fluent Interfaces
- Functional Programming
- Object-Relational Mapping
- Open Source
- Software Design
- Test-Driven Development
- Visual Studio
The opinions expressed on this blog are my own personal opinions. These do NOT represent anyone else’s view on the world in any way whatsoever.
Thank you for visiting my website. I’m a professional software developer since Y2K. A blogger since Y2K+5. Author of Writing Maintainable Unit Tests. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Thinking and learning about all kinds of technologies since forever.