As you build out an application, it is very easy to manually test things. You have been doing this since you started writing your first lines of code! How? Think about it. You write some code, save it, then run the code to see what the result is. Did it give you the result you wanted? Great! Then it worked. So if it is so easy to test the code you write, why write automated tests? The reason is that as your application gets bigger, it takes more and more time to manually test each part of the application.
What are the Benefits of Testing?
Automated testing allows you to test your code frequently in a small amount of time. This means you can find problems and bugs before you actually push your code to production. If you’ve ever deployed an application for the first time, you know the worry that comes with that. Will it work? Are there bugs? Automated testing helps to give you more confidence that things will work when they need to. To be fair, testing is not going to save you from dealing with problems and bugs once software goes into production. It is impossible to write perfect software, no matter how many tests you write. However, instead of deploying an application with 25 bugs, maybe it deploys with only 5 thanks to all of your tests. Another benefit is allowing you to refactor your code with more confidence. In other words, you can clean up your functions, or change the structure of the code itself to make it more readable while still having the code perform the same exact task.
What are the Types of Tests in Software Development?
In general, there are three kinds of automated tests in software development.
- Unit Tests: Test a small unit of an application without external resources like a database.
- Integration Tests: Test the application with all external resources in place.
- Functional / End To End Tests: Test the application through its User Interface.
The Pyramid of Tests
What types of tests should one right when building an application? If we have several types, which is best? Well, most likely you will have a combination of all three types of tests. The most amount of tests would be unit tests since they are easy to write an execute quickly. Next, you would have a collection of integration tests. There would likely be fewer integration tests in the application than unit tests. Lastly, you would have a few end to end tests, as these are the most comprehensive but also the slowest. This approach looks like a pyramid. Note the E2E stands for “End to End” or Functional.
This means you would write most of your tests as unit tests. For any gaps that appear in test coverage, use integration tests. Lastly, use end to end tests the least.
Writing Your First Unit Test
So when do we get to write a test?! We are going to start that right now! First up, we are going to create a javascript-testing directory, then we can use NPM to install Jest. Let’s first use npm init to create a new package.json file.
javascript-testing $npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. Seenpm help json
for definitive documentation on these fields and exactly what they do. Usenpm install
afterwards to install a package and save it as a dependency in the package.json file.Press ^C at any time to quit. package name: (javascript-testing) version: (1.0.0) description: JavaScript Test Tutorial entry point: (index.js) test command: jest git repository: keywords: author: license: (ISC) About to write to C:nodejavascript-testingpackage.json: { "name": "javascript-testing", "version": "1.0.0", "description": "JavaScript Test Tutorial", "main": "index.js", "scripts": { "test": "jest" }, "author": "", "license": "ISC" } Is this OK? (yes)
Now we can install Jest.
Now we should take a look at the highlighted line here in the package.json file. This is saying that when we run npm test
at the command line, the jest
script will run to execute all tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "name": "javascript-testing", "version": "1.0.0", "description": "JavaScript Test Tutorial", "main": "index.js", "scripts": { "test": "jest" }, "author": "", "license": "ISC", "devDependencies": { "jest": "^23.4.1" } } |
Let’s try it and see what happens:
javascript-testing $npm test > javascript-testing@1.0.0 test C:nodejavascript-testing > jest No tests foundIn C:nodejavascript-testing 2 files checked. testMatch: **/__tests__/**/*.js?(x),**/?(*.)+(spec|test).js?(x) - 0 matches testPathIgnorePatterns: \node_modules\ - 2 matches Pattern: - 0 matches npm ERR! Test failed. See above for more details. javascript-testing $
Great! Jest is working but we don’t have any tests yet. Let’s create one! In our project, we can add a tests
folder. In this folder we will place a utility.test.js
file.
The fact that the word test appears in the file name will let Jest know that this is a test. Here is our first test. It doesn’t test anything yet, but this will allow us to run npm test
at the command line and see what happens.
1 2 3 |
test('First Jest Test', () => { }); |
Now we run our test, and check it out!
Above we see what a passing test in Jest looks like. Now, let’s see what a failing test looks like. We can modify our test like so.
1 2 3 |
test('First Jest Test', () => { throw new Error('It crashed!'); }); |
Now when we run it, we get a whole lot of useful information.
How To Test Numbers
We have a function in our utility.js file which creates an absolute number. In other words, it will never return a negative number.
1 2 3 4 5 |
module.exports.absolute = function (number) { if (number > 0) return number; if (number < 0) return -number; return 0; } |
Let’s write a new test to test this. Here is the first iteration.
1 2 3 4 5 6 |
const utility = require('../utility'); test('absolute - should return positive number for any positive input', () => { const result = utility.absolute(1); expect(result).toBe(1); }); |
We can run the test, and it passes.
javascript-testing $npm test > javascript-testing@1.0.0 test C:nodejavascript-testing > jest PASS tests/utility.test.js √ absolute - should return positive number for any positive input (6ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 6.351s, estimated 7s Ran all test suites.
This function is also supposed to return a positive number even when given a negative number. No problem, we can add that to the test.
1 2 3 4 5 6 7 8 9 10 11 |
const utility = require('../utility'); test('absolute - should return positive number for any positive input', () => { const result = utility.absolute(1); expect(result).toBe(1); }); test('absolute - should return positive number for any negative input', () => { const result = utility.absolute(-1); expect(result).toBe(1); }); |
Now when we run the tests, it passes again. Great! Also note how we are shown how many tests ran.
Now let’s make a test fail to show how to use Expected vs Received values to troubleshoot. We know this function should return 0 if it is given 0 as input. We can make a test to fail for this condition like so.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const utility = require('../utility'); test('absolute - should return positive number for any positive input', () => { const result = utility.absolute(1); expect(result).toBe(1); }); test('absolute - should return positive number for any negative input', () => { const result = utility.absolute(-1); expect(result).toBe(1); }); test('absolute - should return 0 if input is 0', () => { const result = utility.absolute(-1); expect(result).toBe(0); }); |
Look at the output we get from a failing test.
That was just to show what the failed test looks like. We can fix that test, and now here is what the success of all 3 tests for this 1 function looks like.
How To Test Strings
Now we can test a function that deals with strings. Consider this simple function that says hello to someone when then provide their name.
1 2 3 |
module.exports.hello = function (name) { return 'Hello ' + name + '!'; } |
Let’s make a test for that. This test will use a new syntax.
1 2 3 4 5 6 7 8 |
const utility = require('../utility'); describe('hello', () => { it('should return the hello message', () => { const result = utility.hello('Stranger'); expect(result).toBe('Hello Stranger!'); }) }); |
The test looks good when we run it.
javascript-testing $npm test > javascript-testing@1.0.0 test C:nodejavascript-testing > jest PASS tests/utility.test.js hello √ should return the hello message (7ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 6.977s Ran all test suites.
Another option you can use when testing strings is the toContain() matcher instead of toBe(). The reason is that the test might be too specific and may fail easily. Below is a slightly more flexible version of the same test.
1 2 3 4 5 6 7 8 |
const utility = require('../utility'); describe('hello', () => { it('should return the hello message', () => { const result = utility.hello('Stranger'); expect(result).toContain('Stranger'); }) }); |
How To Test Arrays
Let’s take a look at some simple array tests we can use in Jest. Imagine we have this function which returns an array of stock tickers.
1 2 3 |
module.exports.getTickers = function () { return ['AAPL', 'MSFT', 'NFLX']; } |
Here is the test we create for this. Check out the docs to understand how that arrayContaining() function works.
1 2 3 4 5 6 7 8 |
const utility = require('../utility'); describe('getTickers', () => { it('should return three stock tickers', () => { const result = utility.getTickers(); expect(result).toEqual(expect.arrayContaining(['NFLX', 'MSFT', 'AAPL'])); }) }); |
How To Test Objects
In this function we pass the id of a game, and the function returns a game object which has that id.
1 2 3 4 5 6 |
module.exports.getGame = function (gameId) { return { id: gameId, price: 10 }; } |
Here is a test that will ensure we get the right game.
1 2 3 4 5 6 7 8 9 10 11 |
const utility = require('../utility'); describe('getGame', () => { it('should return the game with the provided id', () => { const result = utility.getGame(1); expect(result).toEqual({ id: 1, price: 10 }) }) }); |
When we run the test, all looks good!
How To Test Exceptions
For functions that have the ability to throw an error, we need to take a different approach to the test. Consider this function which creates a new user. If the username is not provided, it will throw an exception.
1 2 3 4 5 6 7 8 |
module.exports.createUser = function (username) { if (!username) throw new Error('Username is required.'); return { id: new Date().getTime(), username: username } } |
Here is how we can test a function like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const utility = require('../utility'); describe('createUser', () => { it('should throw an error if username is falsy', () => { const args = [null, undefined, NaN, '', 0, false]; args.forEach(a => { expect(() => { utility.createUser(a) }).toThrow(); }); }) it('should return a user object when a valid username is provided', () => { const result = utility.createUser('Jester'); expect(result).toMatchObject({ username: 'Jester' }); expect(result.id).toBeGreaterThan(0); }); }); |
The test is passing when we run it.
javascript-testing $npm test > javascript-testing@1.0.0 test C:nodejavascript-testing > jest PASS tests/utility.test.js createUser √ should throw an error if username is falsy (9ms) √ should return a user object when a valid username is provided (4ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 6.693s Ran all test suites.
Testing JavaScript With Jest Summary
In summary, we have learned some of the following concepts regarding testing.
- Writing code to test application code is referred to as automated testing.
- Testing helps deliver software with less bugs and higher quality.
- Testing helps you refactor with confidence.
- Jest is a robust testing framework that has everything you need to test JavaScript.
- There are three types of automated tests:
- Unit Tests: Test a small unit of an application without external resources like a database.
- Integration Tests: Test the application with all external resources in place.
- Functional / End To End Tests: Test the application through its User Interface.
- If tests are too general, they won’t ensure your code works. If they’re too specific, they tend to break too easily. Therefore aim for tests that are neither overly general, or overly specific.
- You can use Mock functions to isolate application code from its external resources.
- Jest matcher functions you might use in your testing:
- // Equality
expect(…).toBe();
expect(…).toEqual(); - // Truthiness
expect(…).toBeDefined();
expect(…).toBeNull();
expect(…).toBeTruthy();
expect(…).toBeFalsy(); - // Numbers
expect(…).toBeGreaterThan();
expect(…).toBeGreaterThanOrEqual();
expect(…).toBeLessThan();
expect(…).toBeLessThanOrEqual(); - // Strings
expect(…).toMatch(/regularExp/); - // Arrays
expect(…).toContain(); - // Objects
expect(…).toBe(); // for object references
expect(…).toEqual(); // for equality of properties
expect(…).toMatchObject(); - // Exceptions
expect(() => { someCode }).toThrow();