Introduction to Unit Testing

When code just works, it’s often unappreciated. However, when code breaks, it sticks out like a sore thumb. Unit testing is great method for providing insurance that code is performing as expected and continues to do so when updates occur.

This guide provides an introduction to the concepts of unit testing using Node and Mocha.

Getting Started

There are many valid arguments about crafting unit tests prior to writing application code. However, it’s easier to understand testing starting with existing code.

First, create a new directory and initiate npm with default settings. Then, create two files named nameChecker.js and test/testNameChecker.js. The terminal commands are outlined below.

Then, add the following code to nameChecker.js.

This function accepts a name, compares it against validNames, and returns a message. The function is then exported to be used throughout our application.

Running nameChecker

We’ll write tests eventually, but for now add the following to test/testNameChecker.js. This will import the nameChecker.js module we just created, check if Brutus is valid, and log the response.

Use the following terminal command to run testNameChecker.js.

node test/testNameChecker.js

Result: Brutus is valid!

Adding a Unit Test

Now that we have a working function, it’s time to add test coverage. Start by installing Mocha with the following command. Mocha is a framework which facilitates running and evaluating unit tests.

npm i --save-dev mocha

What is a Unit Test?

Before we write a test, what exactly is a unit test? A unit test should confirm that an individual piece of software consistently performs as expected. In the case of our name checker, we want to confirm that Brutus is a valid name.

Writing a Unit Test

Overwrite test/testNameChecker.js with the following code:

In the above, describe outlines a logical grouping of tests, while it defines what an individual test should achieve. Everything inside the it() function is our first unit test.

The should output “Brutus is valid!” test seeks to confirm when nameChecker is passed Brutus, Brutus is valid! is returned. This is checked by asserting our expected value matches the actual value returned by the function.

Running a Unit Test

Run the test using the command below. This runs a script to invoke mocha’s test functionality.

node_modules/mocha/bin/mocha
Image for post
Image for post
Mocha’s output for successful tests

As you can see above, Mocha runs and confirms that nameChecker invoked with Brutus passes. To see the alternative, change the value passed to nameChecker and run the test again.

Image for post
Image for post
Mocha’s output for failed tests

When a test fails, Mocha provides what caused the test to fail and the expected/actual values that were received. In this case, our assertion failed because we expected a value of Brutus is valid!, but received Invalid name.

Before moving on, modify your test to be passing again.

More Tests!

When we adjusted the value passed to nameChecker, it caused the test to fail. However, we want confirmation that our name checker also functions properly when passed an invalid name. Write another test that achieves this request.

The new test is below, but try writing this test yourself first. Practice is key to learning new skills!

Image for post
Image for post

After your new test, the entire test/testNameChecker.js file should look similar to the following:

The invalid name test is very similar to our first, but the expected value has changed from Brutus is valid! to Invalid name and a different argument is passed to the nameChecker function.

Test-Driven Development

Now that we’ve dipped our toes into the proverbial pool of unit testing, let’s wade a little further and try our hand at test-driven development (TDD).

In TDD, desired functionality is written into tests cases which initially fail. Then, application code is written to satisfy these tests, proving functionality was only created for the designated use cases.

Using tests to validate a software’s output can also be very beneficial when modifying legacy code. When passing tests exist, the internals of the code can be modified, while still confirming the end result is the same.

Modifying nameChecker TDD Style

As we were building nameChecker, Remus heard about the cool new functionality and wanted in on the party. In the name of TTD, first write a test which expects nameChecker invoked with Remus to return Remus is valid!

After you’ve built this test, run your tests and see what happens. If you get stuck, the test code is below.

Image for post
Image for post
Remus’ test is failing

Our test failed, but this is good! We have a test outlining the desired functionality, but it’s currently failing. Let’s modify our code to fulfill the new test case.

Change the validNames array in nameChecker.js from

const validNames = ['Brutus']

to

const validNames = ['Remus']

and run the tests again.

Image for post
Image for post
Brutus’ test is failing

Whoops! The test for Remus is now passing, but in doing so it caused the test for Brutus to fail. This illustrates how tests can help confirm new functionality is not impacting any existing code in your application.

Modify validNames once more to the following and run your tests.

const validNames = ['Brutus', 'Remus']
Image for post
Image for post

Congratulations! All the tests passed. You have successfully implemented unit tests and achieved some test-driven development. If you got lost along the way, the final code can be found on my GitHub.

Conclusions

Hopefully this served as an introduction to the concepts of unit testing and illustrated how tests can be beneficial when writing and maintaining software.

Enjoy posts like these? Follow me on Twitter @andepaulj

Solutions engineer, music enthusiast, dog dad

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store