HACKER Q&A
📣 afarrell

How to plan a minimal restructuring to enable TDD


Hi folks,

I’ve just moved teams to be the only Senior Engineer on a team with two other engineers who are looking to learn better practices, a team lead who wants to improve the automated testing practices, and a PM (with an HR background) who wants features not to have such a high variance of time-of-delivery. The codebase is relatively small. I want to restructure things to enable the team to start working in a red-green-refactor loop.

Problems:

1. It will take some time to enable a workflow of regularly running the specs. I think I would need to have help with the planning.

2. I’m worried that if any of the technical leadership outside the team heard about this, they’d be upset that I talked to my PM about planned refactoring. Martin Fowler says that a perfect team should never need to do a planned refactoring. So our culture is to only refactor incrementally as we go along....which makes about as much sense to me as only washing dishes when you are cooking. (Note: My PM is fine with it so long as it is well-communicated)

3. I don’t know how to make the plan.

I’m especially nervous because I don’t know how long planning will take.


  👤 softwaredoug Accepted Answer ✓
Start outside-in, then decompose to unit tests over time.

Most code bases interface with something with a relatively stable/published interface. Like a database or network library.

If you can find these points, you can mock where you interact with the outside world, then test the entire code base end to end. Over time you can refactor the insides to be more decomposed and testable.

As an example, a code base I worked on involved interacting with network firmware, then sending events to a UI for display. It also received these commands from the UI and told the firmware to do certain things. With some work, we could mock the source and sink of these APIs and inject them as dependencies (instead of instantiating directly). This let us test our huge ball of code to see if it did what was expected. Did the code send the right message to firmware when it got certain UI commands? Did it get in a good or bad state when the firmware responded incorrectly?

This took a bit of work to mock and isolate how our code talked to these interfaces, but once in place, it was a game changer. We could pretty easily isolate problems in our codebase and recreate issues in a testable way.

Then we would take the next step and decompose further, usually on a case by case basis, depending on the cost/benefit of refactoring. But it’s not realistic you’ll get to everything being decomposed in a legacy system.


👤 rmb177
Working Effectively with Legacy Code by Michael Feathers is a good resource for how to introduce testing code into an existing system:

https://www.amazon.com/Working-Effectively-Legacy-Michael-Fe...


👤 danieka
I think that going back and adding tests for existing code is both difficult and, to be honest, a bit boring/unmotivating. Writing tests is also a skill that takes time to acquire. I think the best way to go about this is to do it incrementally, that would also solve your second point.

1. Start writing regression tests when fixing bugs

2. Start writing tests for new code

3. Start writing tests for old code whenever you touch it

Writing regression tests are a bit simpler since the bug report already contains the test case. The odds for the test being useful is higher since this is, in fact, code that can fail.

Writing tests for new code is also easier since you have a greater ability to form the new code to be easy to test. For unit testing you will, probably, naturally favour pure functions/components.

It is super important that your tests run automatically on any PR or merge to master. If they are not run automatically people will break them and it will be a hassle to fix.

One tip for forcing yourselves to write test for new code is using coverage thresholds. For Jest it works like this [0], but remember to also use the collectCoverageFrom to include files that have no tests. Checking thresholds will make it impossible to merge code that lowers the thresholds. Since you already have buy in this is a great way to keep you and your team honest.

And lastly, this was focused on unit tests, but in your situation I would probably begin by writing some high level, black box, end to end test. These give you much more bang for the bucks when it comes to answering the question "Have we screwed up something big or can we release this?".

[0]: https://jestjs.io/docs/configuration#coveragethreshold-objec...


👤 muzani
Have you read the original TDD book by Kent Beck? There's a lot of tips inside. Part of it is starting with what's easy but not obvious. Like a 1+1 might be too obvious, but testing addition and deletion from a list might not be. You don't need a detailed plan. It might also help to just build tests moving forward rather than changing the structure.

The book handholds you through getting stuck on things so I highly recommend it. Tips include: If you know what to type, then type the Obvious Implementation. If not, fake it. If it's still not clear, write more tests and triangulate.

Also remember that refactoring without tests is not really refactoring. So ironically writing the tests first will help a lot.


👤 thedevindevops
Just checking, have you got source control set up? Have you got continuous integration set up? Have you got high level design documentation? Does your stack support a dependency injection framework - not essential but handy. What tech/tools is the team already familiar with?

👤 Jugurtha
>I’ve just moved teams to be the only Senior Engineer [...] I want to restructure things to enable the team to start working in a red-green-refactor loop.

What's the team currently doing, and why?

>So our culture is to only refactor incrementally as we go along....which makes about as much sense to me as only washing dishes when you are cooking.

It's more like avoiding the trap of thinking "we have a pile of dirty dishes from the past several days, why even bother wash the dishes we used today".

Something useful to reduce technical debt is at least not incurring more of it starting from today. You still have the debt from the past that you'll pay gradually, but not adding to it is a start.

Engineering is about tradeoffs which is why extreme people tend to have last programmed in 1981, or are great at architecture diagrams and cute acronyms. I'm busting nibbles.

>I want to restructure things to enable the team to start working in a red-green-refactor loop.

In my opinion, you add the tests then you restructure things because you'd have the tests safety net that tell you and your team that you broke something.

So...

I'll assume you're using GitHub or GitLab.

Right now, you can add a test that succeeds all the time, then configure the CI on GitLab (adding a minimal `gitlab-ci.yml` just to have a pipeline, trigger a CI job that's triggered when you push your changes, and display the coverage and status badges on the `README.md`.

Once you have a pipeline that practically does nothing but that works, you can get to work with the following approaches:

- New code should include tests (independent effort to get things under control)

- Look at the code coverage, and start writing tests for the untested code. As you are new to the team, this will speed up your understanding of the codebase. You also are being useful for the team actually putting your money where your mouth is.

- Add issue templates to the repository for features, bugs, and incidents. This reduces the varience in issue quality and the team members aren't faced with a blank page staring at them. The issues will have a nice structure with comments on how to fill them so they be the most useful. After a while, the members will be able to parse these issues really fast. The issues will have tags (bugs, features) and you'll be able to quickly filter and focus on bugs. It'll also surface similarities. If a bunch of bugs are about a certain part of the system, a module, or a functionality, you add a test harness first, and then you can refactor it or improve it so it sucks less.

- You do that systematically, you'll see consistent progress without the whole thing being a big deal or stressful because it doesn't require "major changes at once" either on the code or on the workflow.

This is what we did for our team. Add scaffolding. Gradually, but consistently improve the workflow. It's a good habit and the process must not get in the way, or you'll have non-consumption and people won't do it.

Here's more: https://news.ycombinator.com/item?id=26552842