A Guide to Test-Driven Development (TDD): Shorter Feedback Loop, Faster Workflow

Trust me, I used to hate testing too.

Mario Hoyos
Bits and Pieces

--

Testing is a love-hate relationship

A short story

Like many before me, I was hesitant to try my hand at test driven development. I always cited reasons such as having twice the code to maintain and/or spending time shipping instead of writing extra code, which was my way of justifying being lazy.

Ultimately, what sold me on the practice was seeing how much shorter my feedback look became. If you want to be able to refactor your code on the fly and ship with confidence, I would urge you to add in at least one layer of tests. I know we do not have all the time in the world, and not all of us have the luxury of a dedicated QA team, but hear me out.

Now, I’m not a TDD zealot nor do I follow the TDD paradigm to the letter. As with anything in development, there are tradeoffs and you need to consider what is going to work for your situation. In my current situation, I’m building an MVP that needs to be shipped within a tight deadline. Though I would love to unit test every line of code, I can’t.

Be pragmatic about what you do and don’t need to test extensively. For example, if you are dealing with people’s money, you better be very confident in your code. That’s a situation that calls for ~%100 code coverage. For most other things, you can likely get away with testing the “happy path” and some of the obvious error states.

What has given me the most bang for my buck is writing integration tests for my backend’s API. Basically, I want to know that when I make requests to my server, I will receive the responses I am expecting. My frontend doesn’t care one bit about the implementation behind this interface. As long as I know that my API is working as expected, I can ship with some level of confidence.

Prior to experimenting with TDD, like most folks, I would write tests AFTER I had written my code. I still sometimes do this (don’t judge me). The issue with this, is that you have to go about testing your code some other way in the meantime, usually manually, and this is time intensive.

Computers are okay at fast repetitive tasks, so why don’t we let them do the heavy lifting?

If we write our tests first, it gives us a chance to think about the interface we are building and how we want it to behave. Sometimes you even have a specification from your boss for how an endpoint should work, and in this situation, all you have to do is convert that spec into tests. Not only will you get feedback as you work on that endpoint, but you’ll also have a suite of tests ready to fire afterward. This not only gives you confidence that your code works as your boss expects it to work, but it also gives you the confidence to refactor fearlessly, since you can check immediately whether you broke something or not.

Enough preaching. We could argue about the benefits of TDD all day, but instead, I would like to give you an example of my approach at work.

Tip: when working with reusable components, use Bit to easily make each of them reusable and even test it in isolation. When you run the tests for every Lego piece and see the results, it’s easier to build new things right?

Let’s write some code.

Let me show you more or less how I would approach building an endpoint from scratch using TDD, trying to highlight the benefits along the way.

You can either follow along below or pull down the finished repo from https://github.com/mariohoyos92/tdd-example.

In order to follow along, you’ll need to have Node/NPM installed on your machine.

Let’s initialize our new project:

npm init
// Hit "enter" through all of the prompts

Now let’s go ahead and install the packages that we are going to use.

npm install --save express supertest jest

We are using Express as the server we will be testing, Supertest to be able to make requests to our Express server, and Jest to execute our tests. You could substitute any number of options for these packages, but I would highly recommend Jest as your testing framework. There’s a reason why it had the highest satisfaction of all JavaScript technologies in 2018.

Sweet, now let’s create our simple server. Create a file called “server.js” and insert the following code:

const express = require(“express”);
const app = express();
module.exports = app;

All we are doing here is instantiating an Express server, and exporting it so that we can pull it into our test file.

Now, go ahead and create a file called “app.test.js”.

The reason that we specified the “.test” in the filename, is that Jest will automatically look for any file that includes that in order to execute the tests inside of it.

In order for this example to work, we will need some data to play with. Create a file called “data.js” and insert the following into it:

const cars = [
{ make: "Ford", model: "Mustang" },
{ make: "Toyota", model: "Camry" },
{ make: "Kia", model: "Rio" },
{ make: "Hyundai", model: "Elantra" },
{ make: "Tesla", model: "S3" }
];
module.exports = { cars };

Perfect! Now that we have all of the basics set up, let’s go ahead and start getting feedback on our code!

One of the wonderful features that Jest has is the ability to watch your files and execute your tests every time that a file is saved. Let’s go ahead and fire up our tests (I know, I know, we haven’t even written tests yet).

In your “package.json” insert the following script:

"scripts": {
"test": "jest --watch"
}

Then execute it from the command line like so:

npm test

If you have been following along, we now have our first piece of feedback from the terminal!

Jest has some pretty sweet error messages which do a great job of guiding us in the right direction. As you can see, our test suite failed because, well, we haven’t written any tests yet. Let’s change that!

In your “app.test.js” add the following code (I will switch to screenshots here so we get the benefits of syntax highlighting) and save the file:

As you can see, we are bringing in the server we had created earlier, and initialized Supertest with that server. We haven’t made any requests to the server just yet, but we will here shortly.

After you save the file, Jest should rerun your tests and you should get the following:

We have to be careful here. This feedback is wrong. Jest is telling us that our test passed, but we know that we have yet to actually test anything other than whether Jest is working or not.

Let’s go ahead and add some assertions about how we want this endpoint to function.

First, we will specify that if the request is successful, we want the server to return an HTTP status code 200, letting us know that the request was OKAY.

We get the following from Jest:

The important part here is where it tells us what Jest was expecting and what it received. As you can see, it expected the status to be 200, but it received undefined.

We need to figure out how to fix that problem. A good starting point may be to actually create that endpoint on our server and return the expected response to let us know that the endpoint is alive and well. Let’s go ahead and do that.

Update your “server.js” to look like this:

Jest output:

Woah!

That’s a QUICK feedback loop.

We know that the code we wrote is working as expected. It’s pretty obvious, however, that our endpoint is still not doing what we want it to, so let’s add the rest of our assertions.

Based on how we described this test, we expect to receive an array of cars, where each car has make and model properties. Our test could look something like this:

Jest output:

Uh oh.

Looks like Jest was expecting a cars array and is not receiving it. As you can see, we have specified how our endpoint SHOULD work before we’ve even written the code. This way, we can ensure we are writing code that meets a specification instead of changing a specification to match our code.

Let’s go ahead and round out our endpoint:

Jest output:

NICE!

We now have an endpoint that works appropriately, as well as a tests written for it.

As you can see, by writing the tests first, we get valuable feedback at every step along the way. Not only does this allow us to write better code the first time around, but if you ever refactor how that endpoint is implemented (which you likely will) you can still be sure that the interface hasn’t changed.

Instead of shipping broken code and hoping for the best, you can now lean on your computer to tell you instantly if you’ve messed something up.

Sure, you’ve written more code, but I think you’ll find that over the lifetime of the project you’ll end up being able to move faster. Feedback loops are everything.

If you don’t believe me (why should you, you don’t know me) then maybe you will believe Cory House, a man much smarter than me.

Cory is a hot-shot, give him a follow.

Conclusion

I’m pretty sure this isn’t the first article you’ve read about TDD. It’s a big deal in the development world for a reason. It’s very possible that you will scoff at what I’ve presented, and continue to never write any tests. I only know this because I did the same thing for the longest time. I would read an article/book/tweet about why TDD is awesome, and then ignore it.

All I ask is that you give it a shot. You don’t have to commit to doing it for every single bit of code for the rest of eternity (though you’d likely benefit from that), just commit to doing it ONE TIME. Write one test BEFORE you write the code it tests. I was pleasantly surprised with how quickly it became second nature, and as I’ve refactored codebases I’ve worked on, it has saved my skin many times.

Don’t let the zealots get in your head, test what you need to, with the time and resources that you have.

Give it a shot, then come back and tell me if it blew up in your face.

I hope that you have learned something new today! I would appreciate it if you could drop some 👏 or leave a comment below! Also, feel free to follow me on Twitter and Medium :)

--

--

I am a passionate pharmacist-turned-web-developer who wants to help others make the career change.