🃏
testing with Jest
on programminglast updated 12/6/20
Jest is one of the most common JavaScript testing frameworks. Most of this post will be a simple introduction to using it but before that, briefly, here's my case for why tests are worth your time.
Test-driven development (TDD) is an approach to building software where you write tests for new features before building them.
The first benefit is structural. With a large codebase, it's critical that additions and revisions don't break everything else.
The second benefit is psychological. As human beings, we're really bad at multitasking. At both a physical and cognitive level, divided focus impairs our ability to do anything well.
As it applies to programming, TDD separates planning from implementation. We turn ourselves into worse programmers when we take on both at once. Similar to pseudocoding, describe the code you want (tests) and then actually write it.
So we'll create a directory to hold a bunch of different practice problems, not just the example we're walking though below. Next, we can generate a package.json
file and install Jest. The subsequent package.json
file should look similar to what's below.
mkdir practice
cd practice
npm init -y // creating a package.json file with default values
npm install --save-dev jest // installing Jest
// practice/package.json
{
"name": "practice",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Ethan Lee-Tyson <eleetyson@gmail.com>",
"license": "MIT",
"devDependencies": {
"jest": "^26.6.3"
}
}
We only need to edit the key-value pair for the scripts
property so that once we've written tests, we're able to run them.
// practice/package.json
{
"name": "practice",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "Ethan Lee-Tyson <eleetyson@gmail.com>",
"license": "MIT",
"devDependencies": {
"jest": "^26.6.3"
}
}
Now we'll write our tests. Here's how that might look for an example problem: find all matching substrings between two strings. We want to test our solution in a file separate from that solution.
mkdir matched-substrings-js
cd matched-substrings-js
touch matchedSubstrings.js // a file for solving our problem
touch matchedSubstrings.test.js // a file to test that solution
With our problem, we can also make a few assumptions to simplify things:
- given two strings with matches, our return value should be an array of all matching substrings
- given two strings with no matching substrings, our return value should be
false
- the given strings won't contain any spaces or special characters
- casing doesn't matter
Let's write a few tests describing how our solution should work in order to fit these assumptions.
// practice/matched-substrings-js/matchedSubstrings.test.js
const matchSubstrings = require('./matchedSubstrings')
test('takes two strings and returns all matching substrings', () => {
const str1 = 'te'
const str2 = 'test'
expect(matchSubstrings(str1, str2)).toEqual(['t', 'te', 'e'])
})
test('order of strings passed should not matter', () => {
const str1 = 'test'
const str2 = 'te'
expect(matchSubstrings(str1, str2)).toEqual(['t', 'te', 'e'])
})
test('returns case-insensitive matches', () => {
const str1 = 'TE'
const str2 = 'test'
expect(matchSubstrings(str1, str2)).toEqual(['t', 'te', 'e'])
})
test('returns false when given strings with no matching substrings', () => {
const str1 = 'no'
const str2 = 'match'
expect(matchSubstrings(str1, str2)).toBe(false)
})
Ignore that first line for now. We have four tests our solution should pass. Their syntax actually reads a lot like plain English; the first block seems to be checking that, given strings 'te'
and 'test'
, our solution function returns an array ['t', 'te', 'e']
.
We pass the test()
function a name for our test and an anonymous arrow function containing our expectations for that test. We also define a pair of strings that we'll pass to our solution function as arguments.
The third line in our test block is the most important:
test('takes two strings and returns all matching substrings', () => {
const str1 = 'te'
const str2 = 'test'
expect(matchSubstrings(str1, str2)).toEqual(['t', 'te', 'e'])
})
When we call expect()
, we pass it a different function as an argument: matchSubstrings()
. This is our solution function from the solution file — we're checking whether it returns what we want it to. Here's one way we could have written that.
// practice/matched-substrings-js/matchedSubstrings.js
function matchSubstrings(str1, str2) {
const substrings = []
const s1 = str1.toLowerCase()
const s2 = str2.toLowerCase()
// iterate over each substring in the 1st string
// if the 2nd string doesn't include the substring and the substring isn't in the array yet, add it
for (let i = 0; i < str1.length; i++) {
for (let j = i + 1; j <= str1.length; j++) {
let substr = s1.slice(i, j)
if (s2.includes(substr) && !substrings.includes(substr)) {
substrings.push(substr)
}
}
}
return substrings.length !== 0 ? substrings : false
}
module.exports = matchSubstrings
Let's focus on that last line. We're exporting our solution function so that we can import it in our test file and call it in test blocks.
// practice/matchedSubstrings/matchedSubstrings.js
function matchSubstrings(str1, str2) {
...
return substrings.length !== 0 ? substrings : false
}
module.exports = matchSubstrings
// practice/matchedSubstrings/matchedSubstrings.test.js
const matchSubstrings = require('./matchedSubstrings')
...
Back to our first test block. We're checking whether the return value of expect()
— which will be the return value of matchSubstrings()
when passed two strings 'te'
and 'test'
— is equal to an array ['t', 'te', 'e']
.
The toEqual()
function (more on this here) compares all properties between objects. In this case, it's comparing whether our two arrays contain the same elements. If matchSubstrings(str1, str2)
returns an array with substrings 't', 'te', 'e'
, toEqual()
returns true and our first test passes.
// practice/matched-substrings-js/matchedSubstrings.test.js
const matchSubstrings = require('./matchedSubstrings')
test('takes two strings and returns all matching substrings', () => {
const str1 = 'te'
const str2 = 'test'
expect(matchSubstrings(str1, str2)).toEqual(['t', 'te', 'e'])
})
Finally, we're able to run this test file from our top-level practice
directory.
npm test -- matchedSubstrings.test
Every programming language has its own set of testing frameworks (ex. RSpec and MiniTest in Ruby), but that's for a separate post. Feel free to use my repo as a point of reference and check out the Jest docs if you want to learn more. Until next time 👋