Refactor code leveraging TDD even without writing tests

August 14, 2020

It all starts fairly simple. You have a day based representation of a calendar at hand and you get a requirement to transform it into the event based counterpart to be consumed by a service that only supports that specific format.

A day based representation of the calendar would look more or less like this, in JSON:

[
  {
    "date": "2017-06-01",
    "status": "unavailable"
  },
  {
    "date": "2017-06-02",
    "status": "unavailable"
  },
  {
    "date": "2017-06-03",
    "status": "booked",
    "bookingId": "abc123"
  },
  {
    "date": "2017-06-03",
    "status": "booked",
    "bookingId": "abc123"
  }
]

The calendar based counterpart would be more succinct, like so:

[
  {
    "type": "Calendar block",
    "range": {
      "start_date": "2017-06-01",
      "end_date": "2017-06-02"
    }
  },
  {
    "type": "Reservation",
    "id": "abc123",
    "range": {
      "start_date": "2017-06-03",
      "end_date": "2017-06-04"
    }
  }
]

So you build a service to do that transformation. It does the job in memory. It does it well and it performs reasonably for the use case you’re looking to fulfil.

To get there, you write the first spec, build the production code, make it pass. You then add the second spec, build the production code, make it pass. Perhaps no refactoring required thus far - all good. You continue to do this and eventually, after a spec passes, you’re not completely happy with something, so you refactor a bit. Oops, the spec is not passing. You spot the error, fix it and it’s back to green! All good… You do this enough times that eventually you’re happy that your specs cover all the bases, your service is ready for a PR/MR. Happy days!

Later on, a new requirement comes in and now there’s a different type of event. Nothing to worry about, you utilise TDD to build it and the design of the service remains solid, you have a few more specs to cover for the additional behaviour. All good!

New requirements come in and it might be that one now is about supporting a different perspective on how consecutive days can be merged depending on a specific rule for a particular type of event (a different status to the ones you had supported up to that point), one that relies on an internal note that gets inserted into calendar day objects with that particular status. Still, nothing to worry about! TDD still serves you well, the end result is good!

The service evolves over time and of course, you’re not the only only contributing to it. There are fellow TDD practitioners that do the same.

Fast forward a few months and you come across that file/class/service again. You’re a reviewer of a PR where that file has a tiny change. But you realise that something seems a bit different.

You expand the file and look at it in its entirety and you realise that the requirements got a bit more complicated and the service now is doing substantially more than you anticipated it would do when you first created it. Perhaps time to apply the Extract method? is your first thought.

You get curious about how many different people contributed to the file. So you might be tempted to look at how many different contributors the file has had over time. Assuming you don't have local changes, you could get a sense by checking the different emails associated with changes by running this in the command line (or something similar to it):

git blame app/.../<your_class_filename.ext> -e | awk '{print $2}' |  sort | uniq

You realise at this point that the file has been modified by more that just 1 or 2 different contributors. That’s pretty normal in your team. The power of collaboration in action!

Perhaps different perspectives took to different ways of interpreting the problem. That combined with not enough refactoring along the way perhaps led to the current state of the class - which in fairness still works as required but has now gotten arguably more complicated than it needed to be, quite a bit more difficult to reason about and change.

You’re particularly unhappy about the current cyclomatic complexity of some private methods of the class. You don’t even measure it or see Rubocop (if you’re using Ruby) complaining about it, but you think you got a pretty good handle on estimating it and you’re convinced you don’t need to measure formally - perhaps you should?

You look at the specs and those are quite long, however still easy to understand. The production however not so much... What should you do?

TDD has allowed you and your team to build a good safety net around that class. Perhaps you rewrite it from scratch? Sounds radical? The class has around 100 lines. It’s not that much…

You decide to go for a rewrite. You think it will take 30 minutes, certainly no more than 1 hour to get it done. Perhaps it would be a good idea to pair but the thrill of rewriting a piece of code alone, quickly, without having to write a single spec in the process (or the foreseeable future) is too tempting?

You’re really keen to just crack on and create a better version of that class using more consistent naming, a simpler algorithm that’d make you happier when you next need to look at that class - if it can be better performing even better, even though that’s not your main concern right now.

So you do it…

You rewrite it. It feels pretty good. It’s liberating!

There’s something that feels different to “orthodox” TDD. It is serving you well in this instance, but the flow is unusual. You didn’t exactly write the tests first. You didn’t even write many tests at all. Still, you’re taking advantage of your TDD practise to do the rewrite with extreme confidence that you’ll get the result you want efficiently.

While you’re at it, your boss asks you to run a SQL report for them. It takes 5 mins, but you end up responding to a few different messages using your team’s messaging platform.

You resume and get most specs passing relatively quickly and then the last 1/2 take a bit more, but you’re pretty happy that you’ve managed to find a way to get it done more or less as you imagined. You’re happy with the result.

You propose the changes as an enhancement to be incorporated as part of the PR that led you back to this class. Of course, before embarking on the journey you had discussed your concerns with the author of the PR and agreed the class could be improved.

It’s been a good afternoon. Perhaps you didn’t do exactly all of what you had in mind, but you feel like you’ve improved the codebase a bit. Just a bit, but an important bit. You feel like if you managed to do small improvements of this scale every day, the codebase would get substantially better after 3 months. Even more a year after. Not that it is currently in bad shape, but there's always room for improvement.

It would become easier to contribute to, more enjoyable to work with. It would be more satisfying to work on it! In the long run, it might make a ton of difference and mean you and your team are substantially happier!

You realise this was possible because TDD served you well, once more! You’re reminded how cool TDD is! Even when you didn’t write tests (first) - most tests you needed had been written before!

You end your work day thinking: what small thing could I change tomorrow that would make the codebase just a tiny bit better?

And you quickly find one! So you plan to do it tomorrow! And you say to yourself and your colleagues: let’s do it! And finally, after dinner, you write a slightly silly blog post about it to share with your workmates and friends!

TDD for the win!