Unit Testing Basics
Learn the basics of unit tests.
Start without tools
Let’s start with an example of unit testing a function in plain JavaScript. We’re using just code, no tools.
The trivial function we’re using is a flatMap implementation.
We’ll take four test cases and implement them to ensure our function adheres to the standard.
Test case 1
var arr = [1, 2, 3, 4];
arr.flatMap((x) => [x, x * 2]);
// [1, 2, 2, 4, 3, 6, 4, 8]
The code takes an array of numbers and applies the mapping function x => [x, x * 2]
to each. This results in an array of arrays, [[1, 2], [2, 4], [3, 6], [4, 8]]
, and flattens that to one array.
The expected outcome is [1, 2, 2, 4, 3, 6, 4, 8]
.
Test case 2
let arr1 = ['it's Sunny in', '', 'California always'];
arr1.map((x) => x.split(' '));
// [['it's','Sunny','in'],[''],['California', 'always']]
arr1.flatMap((x) => x.split(' '));
// ['it's','Sunny','in', '", "California", "always"]
The code takes an array of strings and applies the mapping function x.split(" ")
to each. This results in an array of arrays of different lengths, [["it's","Sunny","in"],[""],["California", "always"]]
, and flattens that to one array.
The expected outcome is ["it's","Sunny","in", "", "California", "always"]
.
Test case 3
let arr1 = [1, 2, 3, 4];
arr1.map((x) => [x * 2]);
// [[2], [4], [6], [8]]
The code takes an array of numbers and applies the mapping function x => [x * 2]
to each. This results in an array of arrays of [[2], [4], [6], [8]]
and flattens that to one array.
The expected outcome is [2, 4, 6, 8]
.
Test case 4
arr1.flatMap((x) => [x * 2]);
// [2, 4, 6, 8]
// Only one level is flattened
arr1.flatMap((x) => [[x * 2]]);
// [[[2]], [[4]], [[6]], [[8]]]
The code takes an array of numbers and applies the mapping function x => [[x * 2]]
to each. This results in an array of arrays of nested arrays, [[[2]], [[4]], [[6]], [[8]]]
, and flattens that to one array.
The expected outcome is [[2], [4], [6], [8]]
, verifying that flatMap only flattens out an array of arrays that’s a single level deep.
//Array.prototype.flatMap = function(cb) {return [];//Return this.map(cb).reduce((acc, n) => acc.concat(n), []);}// Case 1const numbers = [1, 2, 3, 4];const actual = numbers.flatMap(x => [x, x * 2]);const expected = '1,2,2,4,3,6,4,8';console.log('test case 1 passing:', actual.join() === expected);// Case 2const strings = ["it's Sunny in", "", "California"];const actual1 = strings.flatMap(x => x.split(" "));const expected1 = ["it's","Sunny","in", "", "California"];console.log('test case 2 passing:',actual1.join() === expected1.join())// Case 3const numbers2 = [1, 2, 3, 4];const actual2 = numbers2.flatMap(x => [x * 2]);const expected2 = [2, 4, 6, 8];console.log('test case 3 passing:',actual2.join() === expected2.join())// [2, 4, 6, 8]// Case 4// Only one level is flattenedconsole.log('test case 4 passing:', numbers.flatMap(x => [[x * 2]]).join() === [[2], [4], [6], [8]].join()) ;// [[2], [4], [6], [8]]
We implement the initial function like this:
Array.prototype.flatMap = function (cb) {
return [];
};
Then, the code prints out:
test case 1 passing: false
test case 2 passing: false
test case 3 passing: false
test case 4 passing: false
That’s because we haven’t implemented the function yet. It just returns an empty array.
Try to implement the function, or uncomment line 4, replace line 3 with it, and run again.
Now, it should print out:
test case 1 passing: true
test case 2 passing: true
test case 3 passing: true
test case 4 passing: true
The method
- Take the
actual
output of the function. - Compare it to the
expected
. - Report the passing or failure of the test case based on a match between
actual
andexpected
.
Breakdown
-
We start with creating a flatMap polyfill.
-
We then create a placeholder implementation that always returns [].
Test case 1:
-
Assert
flatMap
works with multi-member arrays. -
Prepare input
numbers
. -
Run the function with that and get the
actual
result:const numbers = [1, 2, 3, 4]; const actual = numbers.flatMap((x) => [x, x * 2]);
-
Prepare the
expected
:const expected = '1,2,2,4,3,6,4,8'; console.log('test case 1 passing:', actual.join() === expected);
-
Print out the result of comparing the
actual
to theexpected
.
Test case 2:
-
Assert
flatMap
works with instances of strings. -
Prepare the input
const strings = ["it's Sunny in", "", "California"];
. -
Take the
actual
result:const actual1 = strings.flatMap((x) => x.split(' '));
-
Prepare the
expected
:const expected1 = ["it's", 'Sunny', 'in', '', 'California'];
-
Compare
actual
andexpected
and print out the result:console.log('test case 2 passing:', actual1.join() === expected1.join());
Test case 3
This test case is very similar to test case 1. The only difference is that the callback function produces arrays of length 1.
Test case 4:
-
Assert flatMap works with deeply nested arrays and only flattens one level deep.
-
Take the actual result and compare it with the expected all in a single line:
console.log('test case 4 passing:', numbers.flatMap((x) => [[x * 2]]).join() === [[2], [4], [6], [8]].join());;
Above, the actual
is numbers.flatMap(x => [[x * 2]])
reusing the numbers from the first example. The expected
is [[2], [4], [6], [8]].join()
.
Are we done testing?
These tests make sure that the four specific use cases work. There are lots of other use cases, but we don’t need to test them all.
It’s enough to have test coverage for the broad use case. If a bug appears in our code, we can add tests that capture that bug. In that sense, the tests are never really done.
And that’s the nature of tests: they are ever-growing and changing, just like the codebase!