Introduction
I use Jest nearly every day when working, and it’s a fantastic tool. It lets me ship my code with confidence, knowing that I have produced something which works as intended.
More often than not, when I write tests for my code, I end up catching something I hadn’t considered and go back to take that into account. It saves me from getting getting called in the evening to fix something in production. For me, that’s a big deal.
With that said, I didn’t really know how jest worked. I used it all the time, but had no real clue what was going on under the hood.
Recently, I bought Kent C Dodd’s excellent Testing JavaScript course which has been incredible for increasing my knowledge.
As part of that, he digs into how jest works under the hood, and this encouraged me to try building my own tiny version. So I want to pass on that knowledge to you!
This will be a small series of posts on each part, but you can skip ahead and see the full repo here.
Building our test runner
Today’s focus will be building the actual test runner. This simply accepts a test
with a title, and a callback. We run the callback and if there are no errors, the test passes!
If an error is thrown, then we deem the test to have failed.
Take a look:
const test = async (title: string, callback: Function) => {
try {
await callback();
console.log(chalk.green(`\u2713 ${title}`));
} catch (error) {
console.error(chalk.red(`✕ ${title}`));
console.error(error);
}
So as you can see, we have our test
function which accepts a title
which is a string, and a callback
which is the function which we want to test.
We then run our callback function in a try/catch
block, and assuming that nothing is caught, we say the test has passed.
This allows us to run something like this:
test("sum adds numbers", () => {
const sum = (a, b) => a + b
const result = sum(3, 7)
const expected = 10
expect(result).toBe(expected)
})
So as you can see, we pass a title, and a function where we sum two numbers: 3
and 7
. But on our final line, we see something new: expect
and toBe
. These are our assertions and we have nothing to handle them yet.
Building our assertions
Fundamentally, an assertion is a very simple concept of whether two inputs match. Does sum(3,7)
result in 10
? We don’t want to have to write logic like this for everything:
if (sum(3, 7) !== 10) {
throw new Error("Sums do not match")
}
Instead, we want to extract this out to a helper utility we will call expect
, much like as you see in jest.
const expect = (actual: Function) => {
return {
toBe(expected: String | Object | Array) {
if (actual !== expected) {
throw new Error(`${actual} is not equal to ${expected}`)
}
},
}
}
As you can see, we take in a function which is what we are running our assertion on. We then have a series of methods on a returned object allowing us to expand our test matchers later.
In this case we are wanting a simple toBe
test for an exact match. So we accept a variable called expected
on toBe
and compare that to the result of our function that we passed in earlier.
Going back to our previous example where we add 3
, and 7
, we can do something like this:
expect(sum(3, 7)).toBe(10)
Pretty simple right? Although jest itself is a far more complex tool, this is more or less how it works. We can extend our matchers as well. For example, if we wanted to check for a falsy value, we could do something like this:
toBeFalsy() {
if (actual) {
throw new Error(`${actual} is not falsy.`);
}
},
As you can see, our expect
function can easily be extended to cater for whatever assertions you need to make.
Putting it together, you can access it within our test runner and the above code will return:
✅ sum adds numbers
And that’s it! You’ve just made a very, very simple test runner with some assertions. In the next post in this series, I’ll be showing you how to write your own test mocks which allow you to simulate function calls so that you don’t run them in reality. It’s a very useful technique.
If you want to see the full code, just take a look at the repo!
Top comments (0)