Just crossed 5k follower on dev.to! Thank you everyone! What a fantastic community! Who's on Twitter too? Let's connect => I'm here.
What means testing? How to test JavaScript code with Jest? Learn the basics of testing JavaScript with this Jest tutorial for beginners!
What means testing?
In tech jargon testing means checking that our code meets some expectations. For example: a function called "transformer" should returns the expected output given some input.
There are many types of testing and soon you'll be overwhelmed by the terminology, but long story short tests fall into three main categories:
- unit testing
- integration testing
- UI testing
In this Jest tutorial we'll cover only unit testing, but at the end of the article you'll find resources for the other types of tests.
Jest Tutorial: what is Jest?
Jest is a JavaScript test runner, that is, a JavaScript library for creating, running, and structuring tests. Jest is distributed as an NPM package, you can install it in any JavaScript project. Jest is one of the most popular test runner these days and the default choice for Create React App.
First things first: how do I know what to test?
When it comes to testing, even a simple block of code could paralyze beginners. The most common question is "How do I know what to test?". If you're writing a web application a good starting point would be testing every page of the app and every user interaction. But web applications are also made of units of code like functions and modules that need to be tested too. There are two scenarios most of the times:
- you inherit legacy code which comes without tests
- you have to implement a new functionality out of thin air
What to do? For both cases you can help yourself by thinking of tests as of bits of code that check if a given function produces the expected result. Here's how a typical test flow looks like:
- import the function to test
- give an input to the function
- define what to expect as the output
- check if the function produces the expected output
Really, that's it. Testing won't be scary anymore if you think in these terms: input - expected output - assert the result. In a minute we'll also see an handy tool for checking almost exactly what to test. And now hands on Jest!
Jest Tutorial: setting up the project
As with every JavaScript project you'll need an NPM environment (make sure to have Node installed on your system). Create a new folder and initialize the project with:
mkdir getting-started-with-jest && cd $_
npm init -y
Next up install Jest with:
npm i jest --save-dev
Let's also configure an NPM script for running our tests from the command line. Open up package.json and configure the script named "test" for running Jest:
"scripts": {
"test": "jest"
},
and you're good to go!
Jest Tutorial: specifications and test-driven development
As developers we all like creativity freedom. But when it comes to serious stuff most of the time you don't have so much privilege. More often than not we've got to follow specifications, that is, a written or verbal description of what to build.
In this tutorial we've got a rather simple spec from our project manager. A super important client needs a JavaScript function that should filter an array of objects.
For every object we must check a property called "url" and if the value of the property matches a given term then we should include the matching object in the resulting array. Being a test-savvy JavaScript developer you want to follow test-driven development, a discipline which imposes to write a failing test before starting to code.
By default Jest expects to find test files in a folder called tests in your project folder. Create the new folder then:
cd getting-started-with-jest
mkdir __tests__
Next up create a new file called filterByTerm.spec.js inside tests. You may wonder why the extension includes ".spec.". It is a convention borrowed from Ruby for marking the file as a specification for a given functionality.
And now let's get testing!
Jest Tutorial: test structure and a first failing test
Time to create your first Jest test. Open up filterByTerm.spec.js and create a test block:
describe("Filter function", () => {
// test stuff
});
Our first friend is describe, a Jest method for containing one or more related tests. Every time you start writing a new suite of tests for a functionality wrap it in a describe block. As you can see it takes two arguments: a string for describing the test suite and a callback function for wrapping the actual test.
Next up we're going to meet another function called test which is the actual test block:
describe("Filter function", () => {
test("it should filter by a search term (link)", () => {
// actual test
});
});
At this point we're ready to write the test. Remember, testing is a matter of inputs, functions, and expected outputs. First let's define a simple input, an array of objects:
describe("Filter function", () => {
test("it should filter by a search term (link)", () => {
const input = [
{ id: 1, url: "https://www.url1.dev" },
{ id: 2, url: "https://www.url2.dev" },
{ id: 3, url: "https://www.link3.dev" }
];
});
});
Next up we're going to define the expected result. As per spec the function under test should leave out the objects whose url property does not match the given search term. We can expect for example an array with a single object, given "link" as the search term:
describe("Filter function", () => {
test("it should filter by a search term (link)", () => {
const input = [
{ id: 1, url: "https://www.url1.dev" },
{ id: 2, url: "https://www.url2.dev" },
{ id: 3, url: "https://www.link3.dev" }
];
const output = [{ id: 3, url: "https://www.link3.dev" }];
});
});
And now we're ready to write the actual test. We'll use expect and a Jest matcher for checking that our fictitious (for now) function returns the expected result when called. Here's the test:
expect(filterByTerm(input, "link")).toEqual(output);
To break things down even further here's how you would call the function in your code:
filterByTerm(inputArr, "link");
In a Jest test you should wrap the function call inside expect which coupled with a matcher (a Jest function for checking the output) makes the actual tests. Here's the complete test:
describe("Filter function", () => {
test("it should filter by a search term (link)", () => {
const input = [
{ id: 1, url: "https://www.url1.dev" },
{ id: 2, url: "https://www.url2.dev" },
{ id: 3, url: "https://www.link3.dev" }
];
const output = [{ id: 3, url: "https://www.link3.dev" }];
expect(filterByTerm(input, "link")).toEqual(output);
});
});
(For learning more about Jest matchers check out the documentation).
At this point you can give it a shot with:
npm test
You'll see the test fail spectacularly:
FAIL __tests__/filterByTerm.spec.js
Filter function
✕ it should filter by a search term (2ms)
● Filter function › it should filter by a search term (link)
ReferenceError: filterByTerm is not defined
9 | const output = [{ id: 3, url: "https://www.link3.dev" }];
10 |
> 11 | expect(filterByTerm(input, "link")).toEqual(output);
| ^
12 | });
13 | });
14 |
"ReferenceError: filterByTerm is not defined". That's a good thing actually. Let's fix it in the next section!
Jest Tutorial: fixing the test (and breaking it again)
What's really missing is the implementation of filterByTerm. For convenience we're going to create the function in the same file where the test lives. In a real project you would define the function in another file and import it from the test file.
For making the test pass we'll use a native JavaScript function called filter which is able to filter out elements from an array. Here's a minimal implementation of filterByTerm:
function filterByTerm(inputArr, searchTerm) {
return inputArr.filter(function(arrayElement) {
return arrayElement.url.match(searchTerm);
});
}
Here's how it works: for each element of the input array we check the "url" property, matching it against a regular expression with the match method. Here's the complete code:
function filterByTerm(inputArr, searchTerm) {
return inputArr.filter(function(arrayElement) {
return arrayElement.url.match(searchTerm);
});
}
describe("Filter function", () => {
test("it should filter by a search term (link)", () => {
const input = [
{ id: 1, url: "https://www.url1.dev" },
{ id: 2, url: "https://www.url2.dev" },
{ id: 3, url: "https://www.link3.dev" }
];
const output = [{ id: 3, url: "https://www.link3.dev" }];
expect(filterByTerm(input, "link")).toEqual(output);
});
});
Now run the test again:
npm test
and see it passing!
PASS __tests__/filterByTerm.spec.js
Filter function
✓ it should filter by a search term (link) (4ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.836s, estimated 1s
Great job. But have we finished testing? Not yet. What it takes to make our function fail?. Let's stress the function with an upper-case search term:
function filterByTerm(inputArr, searchTerm) {
return inputArr.filter(function(arrayElement) {
return arrayElement.url.match(searchTerm);
});
}
describe("Filter function", () => {
test("it should filter by a search term (link)", () => {
const input = [
{ id: 1, url: "https://www.url1.dev" },
{ id: 2, url: "https://www.url2.dev" },
{ id: 3, url: "https://www.link3.dev" }
];
const output = [{ id: 3, url: "https://www.link3.dev" }];
expect(filterByTerm(input, "link")).toEqual(output);
expect(filterByTerm(input, "LINK")).toEqual(output); // New test
});
});
Run the test ... and it will fail. Time to fix it again!
Jest Tutorial: fixing the test for uppercase
filterByTerm should account also for uppercase search terms. In other words it should return the matching objects even if the search term is an uppercase string:
filterByTerm(inputArr, "link");
filterByTerm(inputArr, "LINK");
For testing this condition we introduced a new test:
expect(filterByTerm(input, "LINK")).toEqual(output); // New test
For making it pass we can tweak the regular expression provided to match:
//
return arrayElement.url.match(searchTerm);
//
Rather than passing searchTerm straight away we can construct a case insensitive regular expression, that is, an expression that matches regardless of the string's case. Here's the fix:
function filterByTerm(inputArr, searchTerm) {
const regex = new RegExp(searchTerm, "i");
return inputArr.filter(function(arrayElement) {
return arrayElement.url.match(regex);
});
}
And here's the complete test:
describe("Filter function", () => {
test("it should filter by a search term (link)", () => {
const input = [
{ id: 1, url: "https://www.url1.dev" },
{ id: 2, url: "https://www.url2.dev" },
{ id: 3, url: "https://www.link3.dev" }
];
const output = [{ id: 3, url: "https://www.link3.dev" }];
expect(filterByTerm(input, "link")).toEqual(output);
expect(filterByTerm(input, "LINK")).toEqual(output);
});
});
function filterByTerm(inputArr, searchTerm) {
const regex = new RegExp(searchTerm, "i");
return inputArr.filter(function(arrayElement) {
return arrayElement.url.match(regex);
});
}
Run it again and see it passing. Good job! As an exercise for you write two new tests and check the following conditions:
- test for the search term "uRl"
- test for an empty search term. How the function should deal with it?
How would you structure these new tests?
In the next section we'll see another important topic in testing: code coverage.
Jest Tutorial: code coverage
What is code coverage? Before talking about it let's make a quick adjustment to our code. Create a new folder inside your project root called src and create a file named filterByTerm.js where we'll place and export our function:
mkdir src && cd _$
touch filterByTerm.js
Here's the file filterByTerm.js:
function filterByTerm(inputArr, searchTerm) {
if (!searchTerm) throw Error("searchTerm cannot be empty");
const regex = new RegExp(searchTerm, "i");
return inputArr.filter(function(arrayElement) {
return arrayElement.url.match(regex);
});
}
module.exports = filterByTerm;
Now let's pretend I'm a fresh hired colleague of yours. I know nothing about testing and instead of asking for more context I go straight inside that function for adding a new if statement:
function filterByTerm(inputArr, searchTerm) {
if (!searchTerm) throw Error("searchTerm cannot be empty");
if (!inputArr.length) throw Error("inputArr cannot be empty"); // new line
const regex = new RegExp(searchTerm, "i");
return inputArr.filter(function(arrayElement) {
return arrayElement.url.match(regex);
});
}
module.exports = filterByTerm;
There is a new line of code inside filterByTerm and seems it's not going to be tested. Unless I tell you "there's a new statement to test" you're not going to know exactly what to test in our function. It's almost impossible to imagine all the paths our code can take and so arises the need for a tool that helps uncovering these blind spots.
That tool is called code coverage and it's a powerful utensil in our toolbox. Jest has built-in code coverage and you can activate it in two ways:
- via the command line by passing the flag "--coverage"
- by configuring Jest in package.json
Before running the test with coverage make sure to import filterByTerm in tests/filterByTerm.spec.js:
const filterByTerm = require("../src/filterByTerm");
// ...
Save the file and run the test with coverage:
npm test -- --coverage
Here's what you get:
PASS __tests__/filterByTerm.spec.js
Filter function
✓ it should filter by a search term (link) (3ms)
✓ it should filter by a search term (uRl) (1ms)
✓ it should throw when searchTerm is empty string (2ms)
-----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files | 87.5 | 75 | 100 | 100 | |
filterByTerm.js | 87.5 | 75 | 100 | 100 | 3 |
-----------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
A nice summary of the testing coverage for our function. As you can see line 3 is uncovered. Try to reach 100% code coverage by testing the new statement I've added.
If you want to keep code coverage always active configure Jest in package.json like so:
"scripts": {
"test": "jest"
},
"jest": {
"collectCoverage": true
},
You can also pass the flag to the test script:
"scripts": {
"test": "jest --coverage"
},
If you're a visual person there's also a way to have an HTML report for code coverage, it's simply as configuring Jest like so:
"scripts": {
"test": "jest"
},
"jest": {
"collectCoverage": true,
"coverageReporters": ["html"]
},
Now every time you run npm test you can access a new folder called coverage in your project folder: getting-started-with-jest/coverage/. Inside that folder you'll find a bunch of files where /coverage/index.html is a complete HTML summary of the coverage for your code:
If you click on the function name you'll also see the exact untested line of code:
Neat isn't? With code coverage you can discover what to test when in doubt.
Jest Tutorial: how to test React?
React is a super popular JavaScript library for creating dynamic user interfaces. Jest works smoothly for testing React apps (both Jest and React are from Facebook's engineers). Jest is also the default test runner in Create React App.
If you want to learn how to test React components check out Testing React Components: The Mostly Definitive Guide. The guide covers unit testing components, class components, functional components with hooks, and the new Act API.
Conclusions (where to go from here)
Testing is a big and fascinating topic. There are many types of tests and many libraries for testing. In this Jest tutorial you learned how to configure Jest for coverage reporting, how to organize and write a simple unit test, and how to test JavaScript code.
To learn more about UI testing I highly suggest taking a look at JavaScript End to End Testing with Cypress.
Even if it's not JavaScript related I also suggest reading Test-Driven Development with Python by Harry Percival. It's full of tips and tricks for all the things testing, and covers in depth all the different kind of tests.
If you're ready to take the leap and learn about automated testing and continuous integration then Automated Testing and Continuous Integration in JavaScript is for you.
You can find the code for this tutorial on Github: getting-started-with-jest alongside with the solution for the exercises.
Thanks for reading and stay tuned!
Top comments (5)
Really nice and easy to understand. Thanks for that man!
Happy to help!
Thanks a lot for the brief guide!
Easy to read
you're welcome!
Good stuff! I like that the tutorial is easy to grasp and clear. Short is good, but clear is better!