In this lesson, we’ll get the most fundamental understanding of what an automated test is in JavaScript. A test is code that throws an error when the actual result of something does not match the expected output.
Tests can get more complicated when you’re dealing with code that depends on some state to be set up first (like a component needs to be rendered to the document before you can fire browser events, or there needs to be users in the database). However, it is relatively easy to test pure functions (functions which will always return the same output for a given input and not change the state of the world around them).
Base file to test against:
math.js
const sum = (a, b) => a - b; const subtract = (a, b) => a - b; const sumAsync = (...args) => Promise.resolve(sum(...args)); const subtractAsync = (...args) => Promise.resolve(subtract(...args)); module.exports = { sum, subtract, sumAsync, subtractAsync };
index.js
const { sum, subtract } = require("./math"); let result, expected; result = sum(3, 7); expected = 10; if (actual !== expected) { throw new Error(`${result} is not equal to ${expected}`); } result = subtract(7, 3); expected = 4; if (actual !== expected) { throw new Error(`${result} is not equal to ${expected}`); }
Let’s add a simple layer of abstraction in our simple test to make writing tests easier. The assertion library will help our test assertions read more like a phrase you might say which will help people understand our intentions better. It will also help us avoid unnecessary duplication.
const { sum, subtract } = require("./math"); let result, expected; result = sum(3, 7); expected = 10; expect(result).toBe(expected); result = subtract(7, 3); expected = 4; expect(result).toBe(expected); function expect(actual) { return { toBe(expected) { if (actual !== expected) { throw new Error(`${actual} is not equal to ${expected}`); } } }; }
This is also a common way to write a assetion library, expect() function take a actual value and return an object contains 'toBe', 'toEqual'... functions.
One of the limitations of the way that this test is written is that as soon as one of these assertions experiences an error, the other tests are not run. It can really help developers identify what the problem is if they can see the results of all of the tests.
Let’s create our own test
function to allow us to encapsulate our automated tests, isolate them from other tests in the file, and ensure we run all the tests in the file with more helpful error messages.
const { sum, subtract } = require("./math"); let result, expected; test("sum adds numbers", () => { result = sum(3, 7); expected = 10; expect(result).toBe(expected); }); test("subtract substracts numbers", () => { result = subtract(7, 3); expected = 4; expect(result).toBe(expected); }); function test(title, cb) { try { cb(); console.log(`%c ✔︎ ${title}`, "color: green"); } catch (err) { console.error(`✘ ${title}`); console.error(err); } }
Our testing framework works great for our synchronous test. What if we had some asynchronous functions that we wanted to test? We could make our callback functions async
, and then use the await
keyword to wait for that to resolve, then we can make our assertion on the result
and the expected
.
Let’s make our testing framework support promises so users can use async/await.
const { sumAsync, subtractAsync } = require("./math"); let result, expected; test("sum adds numbers", async () => { result = await sumAsync(3, 7); expected = 10; expect(result).toBe(expected); }); test("subtract substracts numbers", async () => { result = await subtractAsync(7, 3); expected = 4; expect(result).toBe(expected); }); function test(title, cb) { try { cb(); console.log(`%c ✔︎ ${title}`, "color: green"); } catch (err) { console.error(`✘ ${title}`); console.error(err); } } function expect(actual) { return { toBe(expected) { if (actual !== expected) { throw new Error(`${actual} is not equal to ${expected}`); } } }; }
To fix the problem, we can make our testing lib async / await.
async function test(title, cb) { try { await cb(); console.log(`%c ✔︎ ${title}`, "color: green"); } catch (err) { console.error(`✘ ${title}`); console.error(err); } }
Not it works normal again.
These testing utilities that we built are pretty useful. We want to be able to use them throughout our application in every single one of our test files.
Some testing frameworks provide their helpers as global variables. Let’s implement this functionality to make it easier to use our testing framework and assertion library. We can do this by exposing our test
and expect
functions on the global object available throughout the application.
setup-global.js:
async function test(title, cb) { try { await cb(); console.log(`%c ✔︎ ${title}`, "color: green"); } catch (err) { console.error(`✘ ${title}`); console.error(err); } } function expect(actual) { return { toBe(expected) { if (actual !== expected) { throw new Error(`${actual} is not equal to ${expected}`); } } }; } global.test = test; global.expect = expect;
Run:
node --require ./setup-global.js src/index.js
Up to this point we’ve created all our own utilities. As it turns out, the utilities we’ve created mirror the utilities provided by Jest perfectly! Let’s install Jest and use it to run our test!
npx jest