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.
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.
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.
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.
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.
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.
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!
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.
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.
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']
const validNames = ['Remus']
and run the tests again.
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']
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.
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