Test-driven Development Saves the Day

Lars Soldahl
Jumio Engineering & Data Science
5 min readJan 13, 2021

“Automate like you are going to live forever. Document like you are going to die tomorrow.”
~ Michael Sperberg-McQueen

Even though I hate writing tests and testing code, I’ve long been a firm believer in test-driven development. I’ve certainly had moments where I questioned whether it was really worth the effort — usually right around the time I’m writing my 99th unit test. But recently I had an experience that proved just how powerful this approach is. In this post, I’ll share how test-driven development saved my veggie bacon and how to make it work for you.

A Quick Overview of Test-driven Development

In test-driven development, you set your goalposts before you write a single line of code by taking a business concept and turning it into technical terms. For example, for identity verification, you might want to allow the user to upload a photo ID and then add metadata to it. Based on this story, you can turn each part into a test case:

  • Render a dialog.
  • If you click Upload, you should see a file browser.
  • If you insert a file, you should see it in the preview.
  • After the preview, you should see the metadata fields change from read-only to writable.

Now that you have these tests, you can start writing your code. When you start out, all the tests will fail. Once you create the dialog, all but the first test will fail, and so on until you’ve completed the feature. That’s test-driven development. You know exactly what’s still left to do to make the feature work successfully.

A key advantage of test-driven development is that it allows you to trust your code. Gone are the days where you make a change and cross your fingers hoping it didn’t break something. If something breaks downstream, our test suite tells us in 30 seconds exactly where the problem is, making debugging and fixing problems absurdly straightforward.

This is exactly what I experienced a few months ago when we decided to update our front-end framework from Angular 5 to Angular 10.

The Challenge: Replace the Foundation While Living in the House

We chose Angular 5 because it was the major supported version at the time. Angular 6 was just coming out of the lab, and we wanted the most stable version. Angular 7 followed quickly after 6, and then Angular 8 and 9 in short order after that, but we decided not to upgrade quite yet. When Angular 10 came out, however, we were impressed with its new packages, components, and optimizations that we really needed. Plus, we were starting to get warnings about potential vulnerabilities — which is like the canary in the coal mine starting to cough. It was time to upgrade, but we were five versions behind the flagship. How badly was this going to break our code, and how long would it take to fix everything?

I started with the upgrade to Angular 6. Thirty tests failed. I made some upgrades and then upgraded to Angular 7. This time, 25 tests failed. I fixed the issues and continued. By the time I got to Angular 10, only two tests failed. I passed it off to QA, and they only found one issue.

Thanks to our commitment to test-driven development and having over 400 unit tests in place, it took me a little more than two days to upgrade our platform through five versions. The tests made it easy to find out what was functionally wrong without having to search for the symptoms. I didn’t have to go back and test every single feature and scenario; the underlying functionality still worked and the output was the same. I had essentially replaced the foundation of the house while we were still living in it, and it only took a couple days.

Making it Work for You

So what’s the best way to get started with test-driven development? If you’re starting development on a new set of features, write a unit test for each one, breaking it down into reusable chunks.

For existing features, start by writing a unit test for every bug that’s been found. This may sound crazy, but it actually future-proofs your code, because many bugs are often repeated. If a dialog box appears stuck to the side of the screen, write a unit test making sure it never happens again. Unit tests should be the manifestation of how it should work (product requirements) as well as how it shouldn’t (QA’s findings).

Note: If you find that the test is hard to write, that’s a clear indication that the code is going to be hard to maintain. Take a look at how you can simplify the code and break it down into smaller modules. Easy to test = easy to maintain.

There are many tools out there for creating unit tests, but testing separately from your development tool isn’t ideal. Here’s another reason I love Angular: it comes with the Karma test runner built in by default, which allows you to run the tests as part of the project. When you create a component, it creates the functional unit, template file, and test file at the same time. So before you commit code, it’s tested. You can also easily run the unit tests as part of the CI/CD process.

Another way to turbocharge your test-driven development is by creating mock objects. Let’s say you want to write a test that makes sure some data is displayed in a dialog box. If that data comes from a service that has multiple dependencies, all of which need to be up and running before it’ll work, things can get complicated very quickly and slow down your testing process significantly.

By creating a mock service that simulates the service and returns fake data that’s used just for testing this specific scenario, you can make testing much more straightforward. Tools like Angular allow you to easily switch between a live test and a mock test, so you can be sure to do the proper live testing before going into production while being nimble during development. Ideally, you should create a mock for every service you use so that you have standardized, reusable mocks instead of creating one-offs for each test. But while you’re working toward that goal, starting off with mini-mocks is a solid strategy.

Summary

Test-driven development can lead to a crisis of faith when you’re expending a lot of effort upfront without seeing immediate results and just want to dive in and write code. That investment requires you to write code in a more systematic way and saves you a lot of wasted hours of endless debugging and refactoring down the road. It essentially forces you to document the system as you go, and it makes your code super easy to maintain.

In practical terms, this means when a component breaks in a critical customer environment, you don’t get that dreaded phone call telling you that you’re the only one who can fix it and that you need to stay up all night troubleshooting. Robust coverage leads to durable releases. By arming yourself with a powerful suite of tests, you ensure your code works right from the beginning and stays healthy through future iterations wherever it’s deployed.

--

--