Yesterday I facilitated a “legacy code refactoring” session at the Softwerkskammer München Meetup. There were ~50 craftswoman and craftsmen, and all of them were coding, trying to improve some particularily bad code I wrote.
We did three different exercises, each of them for 30 minutes, and in all of them, we tried to bring a piece of bad code under test.
First I gave them a very short (actually too short - but that was on purpose) introduction to the code: The “Babysteps Timer”. This is a very simple GUI program I wrote some time ago, because I needed a short example of really bad code that I could use for refactoring exercises.
Clone it on GitHub if you want to try the exercise yourself: https://github.com/dtanzer/babystepstimer.
The application is just a timer that counts down from 02:00 to zero. Ten seconds before reaching zero, it plays a sound. When it actually reaches zero, it plays a sound and changes the background color to red for a few seconds. When you reset it before it reaches zero, it changes the background color to green for a few seconds. That’s it.
The code is reasonably short (a single class with ~150 lines without the Java boilerplate) and most variables are reasonably well named (for some definition of “reasonably” ;) ). Still it is very hard to change: The class has more than 10 different responsibilities, there are two threads, lots of inner classes, and every small part of the code is coupled to almost everything else.
The code makes testing particularily hard because it makes several calls to
System.currentTimeMillis() and state changes take a long time: After starting the timer, you need to wait a full second (that’s 1 000 000 microseconds!) until you see the timer changing. So when your tests can control how fast time progresses for the application, you can test the application more easily and the tests will run faster.
Exercise 1: Refactor, then test
Sometimes, it is so hard to find seams for testing in a bit of code that it makes sense to first refactor a bit to make the code more testable, and then add some tests. This was our first exercise:
Perform some refactoring to make the code testable, then write a test.
Participants could do any refactorings they wanted - Get rid of all the static stuff, extract inner classes to their own files, extract some methods, rename stuff - Whatever. But their goal should be to write a first test (any kind of test - unit test or integrated test or behavior test) after ~25 minutes of refactoring.
I told them to rely on their IDEs as much as possible to minimize the amount of manual testing, which slows you down considerably because a full timer cycle lasts 2 minutes.
As far as I know, nobody got to the point where they could write a first green test. And that was exactly the point of this exercise: I wanted to show them how hard it is to first refactor, then test. Even when it’s often tempting. Especially in a code base like this, with so much coupling and so little cohesion.
Exercise 2: Test, then refactor
OK, so if we cannot easily make this code testable, maybe we can find a way to test it without changing it. At first, this code looks like there are no seams for testing, but there actually is one: The
JTextPane that contains the whole user interface. You can get the whole HTML from this text pane, and you can invoke it’s
HyperlinkListener to simulate button clicks. So, our second exercise was:
Write some high-level functional tests using the timerPane’s HTML and hyperlink listener. Then start to refactor
I also told them that, once they have a few tests, a good first refactoring would be to try to control the progress of time, because this would speed up their existing tests.
Writing those tests will not make the hard refactorings from Exercise 1 any easier, but at least you’ll have a safety net of tests before you do it. When you do it right, you can write automated test for the whole user-visible functionality without making any big changes, and then speed them up by controlling time. Then you can start the refactoring with fast, automated tests as your safety net.
I don’t think any pair actually did a bigger refactoring. Bot several people told me afterwards that it was an eye-opener for them how easy it was to write the tests before the refactoring, compared to refactoring first.
Exercise 3: Golden Master
I also showed them the Golden Master techniqe and told them that it is sometimes very well suited for testing legacy code. So this was our third exercise:
Add log statements to the code, save the log output as your golden master, and compare future runs to this golden master.
I added Samir Talwar’s Smoke test framework and told them they could use it if they want. Or they could also just save the logs to files and compare them with whatever tool they had. Not everyone could use Smoke because some didn’t have Ruby installed…
Even though the “Baby Steps Timer” is not very well suited for “Golden Master” testing, people were very interested in this exercise. Most didn’t know “Golden Master” before, and some said they wanted to try it on some “real” code.
Fun and Learning
I had a lot of fun yesterday, and I learned a lot. I hope most or all attendees feel the same :) So thanks to everyone at Softwerkskammer München and LV 1871 who made this event possible.
If you want to run a legacy code refactoring session at your meetup or in your company, feel free to use the Baby Steps Timer. If you want some tips for facilitating the session, or if you have some ideas/improvements for me, just contact me - I’ll be happy to help and/or listen. And if you want me to facilitate that session or run a longer training for you, I can do that too: Let’s talk ;)