Now that we know more about unit tests, we are going to learn a new way of using them. So far we have written tests to verify functionality of existing code. Next we are going to use tests to verify functionality of code that does NOT already exist. This may sound odd, but this process has many benefits as we will learn.
As the name sounds, Test-driven development (TDD) is a software development process where the unit tests are written first. However, that doesn't tell the entire story. Writing the tests first and intentionally thinking more about the code design leads to better code. The name comes from the idea of the tests driving the development process.
Before we can start using TDD, we need a list of discrete features that can be turned into unit tests. This will help keep our tests focused on specific functionality which should lead to code that is easy to read. Along the way we will build confidence as we add features.
Note
TDD is a process that some organizations choose to use. Using the TDD process is not required when using unit tests.
With TDD you start with the unit test first. Each test must clearly describe the behavior it is testing.
Example
Example test case for a data parsing project:
Because the test is for a feature that does NOT exist yet, we need to think about how the feature will be implemented. This is the time to ask questions like: Should we add a new parameter? What about an entirely new function? What will the function return?
Example
How could we implement our test case? Remember we aren't writing the code yet, only thinking about the design.
parseData
parseData
function will:data
parameter that gets assigned a string of datadelimiter
parameter that will be used to split the string
into an arrayparseData
will be defined in a moduleNext, write the unit test as if the parameter or function you imagined already
exists. This may seem a bit odd, but considering how the new code will be used
helps find bugs and flaws earlier. We also have to use test utilities such as
expect().toEqual()
to clearly demonstrate that the proposed new code
functions properly.
Example
Next, type out the ideas into an actual test. In this example, the test
references a module and a function that have not been created yet. The code
follows the plan we came up with earlier. Very importantly, there is an
expect().toBeTrue()
that verifies an array is returned.
1const parse = require('../parse-numbers');
2
3describe("parse numbers", function(){
4
5 it("returns array when passed comma separated list of numbers", function(){
6 let items = parse("5,8,0,17,6,4,9,3", ",");
7 expect(Array.isArray(items)).toBeTrue();
8 });
9
10});
Now run the test! The test should fail (or not compile at all) because you have referenced code that does not exist yet.
Finally, write code to pass the new test. In the earlier chapters, this is where you started, but with TDD writing new code is the last step.
Example
To make the new test pass, a file must be created that exports a
parseData
function with logic that satisfies the expected result.
1function parseData(text, delimiter) {
2 return text.split(delimiter);
3}
4
5module.exports = parseData;
Coding this way builds confidence in your work. No matter how large your code base may get, you know that each part has a test to validate its functionality.
Example
Now that we have one passing test for our data parser project, we could confidently move on to writing tests and code for the remaining features.
While adding new features and making our code work is the main goal, we also want to write readable, efficient code that makes us proud. The red, green, refactor mantra describes the process of writing tests, seeing them pass, and then making the code better. As the name suggests, the cycle consists of three steps. Red refers to test results that fail, while green represents tests that pass. The colors refer to test results which are often styled with red for failing tests and green for passing tests.
Red -> Write a failing test.
Green -> Make it pass by implementing the code.
Refactor -> Make the code better.
Refactoring code means to keep the same overall feature, but change how that feature is implemented. Since we have a test to verify our code, we can change the code with confidence, knowing that any error will be immediately identified by the test. Here are a few examples of refactoring:
The refactor is also done in a TDD process: