Unit Testing Basics

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 1
const 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 2
const 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 3
const 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 flattened
console.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

  1. Take the actual output of the function.
  2. Compare it to the expected.
  3. Report the passing or failure of the test case based on a match between actual and expected.

Breakdown

  • We start with creating a flatMap polyfill.

  • We then create a placeholder implementation that always returns [].

Test case 1:

  1. Assert flatMap works with multi-member arrays.

  2. Prepare input numbers.

  3. Run the function with that and get the actual result:

    const numbers = [1, 2, 3, 4];
    
    const actual = numbers.flatMap((x) => [x, x * 2]);
    
  4. Prepare the expected:

    const expected = '1,2,2,4,3,6,4,8';
    console.log('test case 1 passing:', actual.join() === expected);
    
  5. Print out the result of comparing the actual to the expected.

Test case 2:

  1. Assert flatMap works with instances of strings.

  2. Prepare the input const strings = ["it's Sunny in", "", "California"];.

  3. Take the actual result:

    const actual1 = strings.flatMap((x) => x.split(' '));
    
  4. Prepare the expected:

    const expected1 = ["it's", 'Sunny', 'in', '', 'California'];
    
  5. Compare actual and expected 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:

  1. Assert flatMap works with deeply nested arrays and only flattens one level deep.

  2. 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!