Give Codeship a try

Want to learn more?

This article was originally published on RisingStack by Peter Czibik. With their kind permission, we’re sharing it here for Codeship readers.

Making changes to a large codebase and making sure it works is a huge deal in software development. We’ve already talked about a few great features of Node.js testing before, and it is very important to emphasize how crucial it is to have your code tested before you release it to your users.

It can be tedious to have proper test coverage when you have to focus on pushing out all the new features, but think about your future self. Would you like to work on code that’s not tested properly? If not, read this guide on getting testing and TDD (test-driven development) right.

Getting Test-Driven Development (TDD) Right

When new people join a project, you’ll have to make sure that whenever they make a breaking change to the codebase, your tests will indicate it by failing. I have to admit that it is hard to determine what a breaking change is, but there is one thing that I’ve found really handy: TDD.

Test-driven development is a methodology for writing the tests first for a given module and for the actual implementation afterward. If you write your tests before your application code, that saves you from the cognitive load of keeping all the implementation details in mind, for the time you have to write your tests. At least for me, these are the two best things in it. I always found it hard to remember all the nitty-gritty details about the code that I had to test later.

With TDD, I can focus more on the current step that I’m taking. It consists of three steps:

writing failing tests

writing code that satisfies our tests

and refactor

It’s that simple and I’d like to encourage you to give it a try. I’ll guide you through the steps I usually take when I write a new module, and I’ll also introduce you to advanced testing principles and tools that we use at RisingStack.

Step 1: Creating a new module

This module will be responsible for creating and fetching users from our database, PostgreSQL. For that, we’re going to use knex.

I always like to create a function called up that encapsulates the creation of the table. All I currently care about is being able to call this function. So I expect it to be a function, let’s run the tests now:

AssertionError: expected undefined to be a function
at Context.it (lib/User.spec.js:9:29)

This is our first failing test, let’s fix it.

'use strict'
function up () {
}
module.exports = {
up
}

This is enough to satisfy the current requirements. We have such little code that there is nothing to refactor just yet, so let’s write the next test. I want the up function to run asynchronously; I prefer Promises to callbacks, so I’m going to use that in my example.

Step 3: Creating a Node.js test case

What I want is the up function to return a Promise, so let’s create a test case for it:

You see my point now. Always take a small step toward your goal with writing your tests and then write code that satisfies it. It is not only good for documenting your code, but when its API changes for some reason in the future, the test will be clear about what is wrong. If someone changes the up function, use callbacks instead of Promises, so our test will fail.

Advanced testing

The next step is to actually create tables. For that, we will need knex installed.

npm install pg knex --save

For the next step, I’m going to create a database called nodejs_at_scale with the following command in the terminal:

createdb nodejs_at_scale

And create a database.js file to have the connection to my database in a single place.

The Actual Implementation

We could go more in-depth with expecting all of the fields on the table, but I’ll leave that up to your imagination.

Now we are at the refactor stage, and you can already feel that this might not be the cleanest code we’ve written so far. It can get a bit funky with huge Promise chains, so let’s make it a little bit easier to deal with. We are great fans of generators and the co module here at RisingStack; we rely on it heavily on a day-to-day basis. Let’s throw in some syntactic sugar.

If you look at the example above, you’ll see that we can use the yield keyword to extract the resolved value out of the promise, or you can return it (at the end of the function). That way mocha will do that for you. These are some nice patterns you can use in your codebase to have cleaner tests. Remember, our goal is to express our intentions. Pick whichever you feel is closest to yours.

Let’s clean up before and after our tests in a before and after block.

This should be enough for the up function. Let’s continue with creating a fetch function for our User model.

After expecting the exported and the returned types, we can move on to the actual implementation. When I’m dealing with testing modules with a database, I usually create an extra describe block for those functions that need test data inserted.

Within that extra describe block, I can create a beforeEach block to insert data before each test. It is also important to create a before block for creating the table before testing.

Notice that I’ve used lodash to omit those fields that are dynamically added by the database. It would be hard (or even impossible) to inspect on otherwise. We can also use Promises to extract the first value to inspect its keys with the following code:

Testing Internal Functions

Let’s move forward with testing some internals of our functions. When you’re writing proper tests, only the functionality of the current function should be tested. To achieve this, you have to ignore the external function calls. To solve this, there are some utility functions provided by a module called sinon. The sinon module allows us to do three things:

Stubbing: The function that you stub won’t be called. Instead you can provide an implementation. If you don’t provide one, then it will be called as function () {} empty function).

Spying: A function spy will be called with its original implementation, but you can make assertions about it.

Mocking: Basically the same as stubbing but for objects, not only functions.

To demonstrate the use of spies, let’s introduce a logger module into our codebase: winston. Guess what the code is doing by its test here:

with inserted rows
info: lowercase parameter supplied
✓ should return users with timestamps and id
info: lowercase parameter supplied
✓ should return the users by their name
info: lowercase parameter supplied
✓ should call winston if name is all lowercase

The logger was called, and we even verified it through our tests, but it is also visible in the test output. It is generally not a good thing to have your test’s output cluttered with text like that. Let’s clean that up.

To do that, we have to replace the spy with a stub. Remember, I’ve mentioned that stubs will not call the function that you apply them to.

As you can see, it is already a bit tedious to restore all of the stubs by hand at the end of every test case. For this problem, sinon has a nice solution called sandboxing. Sinon sandboxes allow you to define a sandbox at the beginning of the test. When you’re done, you can restore all of the stubs and spies that you have on the sandbox. Check out how easy it is:

There is one last refactor to take on in these tests. Instead of stubbing each property on the fake object, we can use a mock instead. It makes our intentions a little bit clearer and our code more compact. To mimic this chaining function call behavior in tests, we can use the returnsThis method.

Preparing for Failures

These tests are great if everything goes according to plan, but sadly we also have to prepare for failures. The database can sometimes fail, so knex will throw an error. It is really hard to mimic this behavior properly, so I’m going to stub one of the functions and expect it to throw.

With this pattern, you can test errors that appear in your applications. When possible, try to avoid try-catch blocks, as they are considered an anti-pattern. With a more functional approach, it can be rewritten as the following:

Conclusion

While this guide concludes most of what we do here at RisingStack on testing, there is a lot more to learn for us and for you from these projects’ excellent documentation. Links to them can be found below:

If you have made it this far, congratulations, you are now a 5-dan test-master in theory. Your last assignment is to go and fill your codebase with the knowledge you have learned and create greatly documented test cases for your code in TDD style!

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles. Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.

We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.