Why should we write tests for our code?
When there are more than one developers making changes actively to the code base, issues and bugs tend to arise. It is also difficult to troubleshoot on who commited the buggy code, or exactly what is the root cause of the bugs. Therefore, it would be good to have preventive actions taken before introducing any of this into the code base. This can be done by writing tests, it can be tested locally by individual developers on their machines, or automatic test suites can also be setup in the CI/CD pipelines which gets triggered when code commits happened. Another benefit of writing tests is that when we are developing the features for app, we tend to write better and pure functions as the awareness of we would have to write tests for them eventually.
Different types of tests
There are different types of tests and these are the most commonly seen:
Unit test
Unit test is used to test the smallest unit of source code (like functions or methods). This is the easiest to be implemented, and the most common tests among the types.
Integration test
This is to test the cross communication between different components or units in the code base, an example would be authentication functionalities which involves different parts of the app architecture. Integration tests are built under the premise of the individual unit tests are done.
End to end test
End to end test, as the name suggests is to test the workflow of the software from start to finish. This can be really complex when the app grows larger, and therefore a lot of companies still carry out manual testing. The process can start from launching the browser, typing the web app URL in the address bar ..., which is UI-driven. However, there are also tools like Selenium, Cypress and Protractor to help automating these end-to-end testing although it might take quite some time to setup.
There are quite a number of testing libraries, serve different purposes and for different programming languages out there. We are going to focus on testing our JavaScript code in this article. More specifically, Jest is the main character of this article.
Jest: What and Why?
Jest is a popular (especially for React library) JavaScript testing library. It provides a wide variety of methods and funtionalities which cover many parts including assertions, mocks and spies, code coverage and etc in a testing process. When you use create-react-app framework, Jest has already been built in. In today's article, we are going through the simple setup of Jest for your JavaScript code and how we can start locally testing our app functionalities.
Quick Setup
First, we initialize the work dir with npm.
npm init -y
The -y flags basically means accepting the prompts automatically from npm init (instead of pressing enter to each prompt).
Next, We install Jest from npm. We only need to install Jest as dev dependencies because it is only required for development phase.
npm install jest --save-dev
After installing, you should see the Jest package is included in the devDependencies of package.json.
{
"name": "jest-testing",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^27.4.5"
}
}
Now, let's start with our first example:
script1.js
const addNums = (a, b) => {
return a + b;
};
module.exports = addNums;
Script 1 is just adding up two numbers and return the sum.
In order to test script1.js, we create another file called "script1.test.js" (it would be good to follow convention of naming test files for the scripts). In this test script, we can add the following JavaScript code:
const addNums = require('./script1');
it('Function that adds two numbers and return sum', () => {
expect(addNums(4, 5)).toBe(9);
expect(addNums(4, 5)).not.toBe(10);
});
What this does is that we import the addNums function from script1.js and perform test in this script. You can write "test" or its alias "it" (that we used in the script") from Jest to test the addNums function. First argument is gonna be the name of this particular test and the second argument has the expectations to be tested. The method is quite self-explanatory as plain English: Expect the function to add up the number 4 and 5, and the results to be 9. Second line of test is to test passing in 4 and 5 should not produce a result of 10. Easy.
In order to run this test, we need to configure "test" script in package.json to run. You can configure as follow:
"scripts": {
"test": "jest ./*test.js"
}
This is telling Node to run test, and catch the regex of filenames. After you have changed this, run:
npm test
You should receive output like this:
PASS ./script1.test.js
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.125 s
Ran all test suites matching /.\\*test.js/i.
It means that you now have one test suite (script1.test.js) and one test (one "it" is one test).
If you do not wish to type npm test every single time to run the tests, you may configure you test script in package.json as below:
"scripts": {
"test": "jest --watch ./*test.js"
}
Everytime you save a file after making changes, npm test will watch and get trigger automatically to run the tests.
Let's take a look at the second example:
script2.js
const findNames = (term, db) => {
const matches = db.filter(names => {
return names.includes(term);
});
// We only want the first three of search results.
return matches.length > 3 ? matches.slice(0, 3) : matches;
}
const functionNotTested = (term) => {
return `Hello ${term}!`;
};
module.exports = findNames;
Given a db (a JS array), and a search term, return the names that match with the term (only the first 3 matches). The reason that we inject the db as a dependency for this function so that this function is more reusable, and easier to test with mock database.
The function "functionNotTested" does not serve any purpose, but just to show you the test coverages later. We are not going to write test for this function.
There seems to be more things to test in this function. First, we can test if the function returns the expected search results with the provided search term. Secondly, we are expecting the function to return only the first 3 matches of the search term. We can also check if null or undefined is passed into the function for the search term as parameter, the function can handle properly and return empty array. Lastly, we can also make sure that this search function is case sensitive. We do not need to perform real database connection since this is a unit test. We should make sure that this function should work with the injected db array and search term as expected before testing the integration with real db. Therefore, we can simple create a mock db array, and pass into the function (there you go the benefit of writing reusable code). And this is the test script that we can possibly construct:
const findNames = require('./script2');
const mockDB = [
"Kamron Rhodes",
"Angelina Frank",
"Bailee Larsen",
"Joel Merritt",
"Mina Ho",
"Lily Hodge",
"Alisha Solomon",
"Frank Ho",
"Cassidy Holder",
"Mina Norman",
"Lily Blair",
"Adalyn Strong",
"Lily Norman",
"Minari Hiroko",
"John Li",
"May Li"
]
describe("Function that finds the names which match the search term in database", () => {
it("Expected search results", () => {
// This should return empty array as "Dylan" does not exist in the mockDB
expect(findNames("Dylan", mockDB)).toEqual([]);
expect(findNames("Frank", mockDB)).toEqual(["Angelina Frank", "Frank Ho"]);
});
it("This should handle null or undefined as input", () => {
expect(findNames(undefined, mockDB)).toEqual([]);
expect(findNames(null, mockDB)).toEqual([]);
});
it("Should not return more than 3 matches", () => {
expect(findNames('Li', mockDB).length).toEqual(3);
})
it("The search is case sensitive", () => {
expect(findNames('li', mockDB)).toEqual(["Angelina Frank", "Alisha Solomon"])
})
})
This should make total sense to you. If the function encountering a search term that does not exist, or receive null or undefined as search term, the function should return empty array (JavaScript "filter" function handles that). In the last test, we are expecting the search function is case sensitive, and therefore names such as "Lily ..." and "... Li" should not appear in the results. Lastly, the function "describe" is used to group multiple tests together as a whole. Therefore, when the results printed out, these tests will hava a group name called "Function that finds the names which match the search term in database". "toEqual" can be used to test JavaScript objects.
Let's go through the last example:
script3.js
const fetch = require('isomorphic-fetch');
const fetchPokemon = async (pokemon, fetch) => {
const apiUrl = `https://pokeapi.co/api/v2/pokemon/${pokemon}`;
const results = await fetch(apiUrl);
const data = await results.json();
return {
name: data.name,
height: data.height,
weight: data.weight
};
};
module.exports = fetchPokemon;
We will need to call API in the third script, since we are using Node.js (and the browser fetch API is not available), you may install isomorphic-fetch for Node.js:
npm install isomorphic-fetch
The API that we use in this example is PokéAPI. It is handy to retrieve Pokemon information by passing in the Pokemon that you want to find into the API path. This function returns the name, weight and height of the Pokemon found.
Until this point, I would like to introduce another functionality of Jest: providing an overall view of tests coverage to your code.
After you have created "script3.js", run this:
npm test -- --coverage
You should see this:
This shows how much percentage of tests were written to cover each JavaScript file, and which line is not covered. Remember that there was a function in our script2.js that we did not write any test for it, and that is why script2.js does not get 100%. We haven't written any test case for script3.js and therefore, 0% test coverage for it.
Alright, we can start writing test for script3.js, let's try with this test script first:
const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');
it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
fetchPokemon("bulbasaur", fetch).then(data => {
expect(data.name).toBe("bulbasaur");
expect(data.height).toBe(7);
expect(data.weight).toBe(69);
});
})
So, what this script is trying to do is that it tries to call the API, and retrieve the data to be compared with the expected values. Let's try running npm test:
> jest-testing@1.0.0 test C:\Users\Dylan Oh\source\repos\jest-testing
> jest ./*test.js
PASS ./script2.test.js
PASS ./script3.test.js
PASS ./script1.test.js
Test Suites: 3 passed, 3 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 0.801 s, estimated 1 s
Ran all test suites matching /.\\*test.js/i.
Yay! It passed! Or ... is it really?
Well, there is a way to know this. We can add a function to check how many assertions were passed in a test:
expect.assertions(numberOfAssertionsExpected);
Lets add that to our script3.test.js:
const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');
it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
expect.assertions(3);
fetchPokemon("bulbasaur", fetch).then(data => {
expect(data.name).toBe("bulbasaur");
expect(data.height).toBe(7);
expect(data.weight).toBe(69);
});
})
We are expecting 3 assertions to be done here, for name, weight and height respectively. Run npm test:
FAIL ./script3.test.js
● Find the Pokemon from PokeAPI and return its name, weight and height
expect.assertions(3);
Expected three assertions to be called but received zero assertion calls.
3 |
4 | it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
> 5 | expect.assertions(3);
| ^
6 | fetchPokemon("bulbasaur", fetch).then(data => {
7 | expect(data.name).toBe("bulbasaur");
8 | expect(data.height).toBe(7);
at Object.<anonymous> (script3.test.js:5:12)
PASS ./script2.test.js
PASS ./script1.test.js
Test Suites: 1 failed, 2 passed, 3 total
Tests: 1 failed, 5 passed, 6 total
Snapshots: 0 total
Time: 0.842 s, estimated 1 s
Ran all test suites matching /.\\*test.js/i.
npm ERR! Test failed. See above for more details.
Opps... zero assertion call. So what is happening here? The reason is, the assertions do not know anything about the asynchronous call, and before the data is retrieved, the tests have already passed. Therefore, we need a way to tell these assertions to wait until the data has come back.
One way to resolve this is to pass in a "done" function to the test method's call back function, and put it after the assertions.
const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');
it("Find the Pokemon from PokeAPI and return its name, weight and height", (done) => {
expect.assertions(3);
fetchPokemon("bulbasaur", fetch).then(data => {
expect(data.name).toBe("bulbasaur");
expect(data.height).toBe(7);
expect(data.weight).toBe(69);
done();
});
})
And, it passed and ensured that three assertion calls were made.
PASS ./script3.test.js
PASS ./script2.test.js
PASS ./script1.test.js
Test Suites: 3 passed, 3 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 0.868 s, estimated 1 s
Ran all test suites matching /.\\*test.js/i.
Even a simpler way, we could just return this asynchronous function, and Jest is smart enough to wait until the results come back.
const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');
it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
expect.assertions(3)
return fetchPokemon("bulbasaur", fetch).then(data => {
expect(data.name).toBe("bulbasaur");
expect(data.height).toBe(7);
expect(data.weight).toBe(69);
});
})
This will also pass the assertion tests expectation. I personally suggest to use the return statement to return the Promise, and always remember to include number of assertion calls expected for testing asynchronous function to ensure that assertions were really run through.
We can remove the unnecessary function in script2.js, and run npm test -- --coverage once more:
And there we have 100% coverage with the tests.
It is always a good practice to write tests for your code, whether to be tested locally or on the CI/CD pipeline. This will help us to catch the potential bugs earlier and force ourselves to write better code in a way.
In my next article, I would like to cover specific test (such as snapshot test) for React components. Cheers.
Do follow me for more future articles on web design, programming and self-improvement 😊
Top comments (28)
Not a big fan of jest, since it has something about 800 dependencies.
Sadly the ide support for UVU is not great yet, however I'm looking forward.
Currently I prefer mocha for backend node code. For mocks I try to avoid them however if I have to I use sinon
Agreed with that! Because Jest pretty much covers most of the functionalities by itself and it is not as flexible like others :)
Mocha and Sinon makes a good combination too!
you should also mention proxyquire since sinon does not cover everything.
Mocking 3rd party deps is easy with jest, however requires some tinkering with sinon.
Sadly...
Well, I guess nobody is perfect :) Basically, I stick with Jest for the initial setup, and will combine with other libraries when necessary
In my experience that works well. I just got paranoid with the recent supply chain attacks 😂
I like Jest but it quickly becomes a mess when introducing Typescript, and testing backend code.
Mocking named exports is a big mess.
Other than that I like Jest (except for their ties to Facebook evil )
Great article, good for introduction, thanks
Thanks for leaving a comment :)
Yea... It needs a lot of workarounds when it comes to Typescript :)
It does - but I still use Jest as for me its the best all around unit testing framework/library.
For E2E and integration tests, I swear on Cypress, but Jest rules for unit tests.
Cheers, and thanks
Thanks! This article has made me curious to see how jsx is tested.
Sure Rishad! I will try to share more of this in the next article ;)
Besides the problems with Typescript it also suffers from memory leaks. I wouldn't event recommend it. It's just used because of herd effect. Tests are much faster and stable with Mocha.
Thanks for leaving a comment Patrice :)
I see... I haven't read up on its memory leaks issue, but I am interested to read up more and will definitely share my findings on that! Thank you for your suggestions :)
Good
Thanks!
Thank you for this article. This helped in my journey exploring unit testing :)
No problem! Glad this could help :)
Thank you
Code, not codes.
Thanks a lot Paul! I have made the corrections :)
Nice article on testing! Interested to see more of this in the future.
Thanks! Will definitely share more :)
thank you for the article. why did u use jest instead of mocha and chai?
I can't wait to see how to use jest on jsx or tsx hehe
Hey Ragil, thanks for leaving a comment! I generally prefer Jest because it provides a lot of built-in functionalities such as test runner, code coverage etc, without having to combine with other testing tools (it's like all in one) :)
However, Mocha makes a good combination with other libraries such as Sinon.js, Chai.
In order to test React, I planned to use methods from RTL and combination with Jest :)
Some comments have been hidden by the post's author - find out more