Want To Learn JavaScript Unit Testing? Learn Functional Programming First!

If you’re a self-taught JavaScript developer like me, you may not be doing JavaScript unit testing. Self-taught JS developers tend to jump right into coding and skip learning software development fundamentals…like unit testing.

The best way to learn JavaScript unit testing is to realize you should write code in a functional programming style. Functional programming (or, FP) encourages writing small, easy-to-read functions, which are easy to test.

Table of Contents

  1. Before we begin…
  2. Haven’t done JavaScript unit testing yet? That’s fine.
  3. About functional programming
  4. What we’re going to do
  5. The web page for all this
  6. The test suite
  7. A quick note about QUnit
  8. Test-Driven Development (TDD)
  9. About code coverage
  10. Testing more functional programming composition
  11. Bringing it altogether
  12. Further reading
  13. Conclusion

Before we begin…

While this post talks about functional programming, it’s written more as a beginner’s guide to JavaScript unit testing. The post demonstrates how FP makes unit testing easier, but doesn’t discuss FP beyond that.

Even with that, this post isn’t an in-depth JavaScript unit testing tutorial. It covers just enough to get you up and running: assertions, test suites, test coverage and test-driven development.

The resources at the end of this post cover JavaScript unit testing and functional programming in depth.

If you want to follow along with the examples, please read the instructions in the code repo.

Haven’t done JavaScript unit testing yet? That’s fine

First of all, it’s OK if you haven’t regularly unit tested your JavaScript up to this point. This is because unit testing isn’t encouraged in the JS community like it is in other programming communities like Java.

JavaScript was built to be a low barrier of entry for beginner programmers: JS creator Brendan Eich has said that. JS was used for simple things like dropdown menus, rollover effects and cookies when it first came out; therefore, ignoring JS unit testing was acceptable.

But starting with the Great AJAX Revolution of 2005, JavaScript evolved into a full-on application platform. JS lives well on the server via Node, JS stack solutions like MEAN and JAMstack are getting popular and so on.

With JavaScript now at this level, it makes sense to apply traditional software development practices. Like unit testing.

About functional programming

You should test small pieces of code, not big pieces. Functional programming encourages writing functions in small pieces.

The rules of functional programming are:

  • A function should depend on its own scope to work, not its outer scope.
  • Functions definitely shouldn’t change the outer scope.
  • Functions should explicitly return something.
  • If a function gives the same input, it should always produce the same output.
  • Most of all, functions must be small and reusable.

This is just an FP summary: you can read more about it by clicking on this article’s various links. But the last point is the most relevant to JavaScript unit testing:

Functions must be small and reusable.

In addition, functional programming encourages composition: the combining of multiple functions to make another function. This is usually done by passing a function as a parameter to another function, where the passed function gets invoked inside the other one.

What we’re going to do

James Sinclair wrote an excellent four-part tutorial on functional programming. We’ll create unit tests for his function solutions in the tutorial’s first part.

The web page for all this

Here’s what the web page for this, index.html, looks like:


<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Learn JS Unit Testing with Functional Programming</title>
</head>
<body>

  <div id="carousel-one"></div>
  <div id="carousel-two"></div>
  <div id="main-carousel"></div>

  <div id="unicorn"></div>
  <div id="fairy"></div>
  <div id="kitten"></div>

  <a href="test/tests.html" target="_blank">View tests</a>

  <script src="jquery.js"></script>
  <script src="app.js"></script>
  <script src="scripts.js"></script>
</body>
</html>

We have a few page elements that we’ll target in our JS later. We also have a link to our group of tests (or, our test suite) and links to some JavaScript files.

The core jQuery file is here. Other files include app.js and scripts.js: the code we’re testing is in app.js, but that code gets implemented in scripts.js.

The test suite

Our test suite lives in test/tests.html and looks like this:


<!-- test/tests.html -->

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Functional programming test suite</title>

  <link rel="stylesheet" href="qunit.css">
  <script src="qunit.js"></script>

  <script src="../jquery.js"></script>

  <script src="../app.js" data-cover></script>

  <script></script>

</head>
<body>

  <div id="qunit"></div>

  <div id="qunit-fixture"></div>

</body>
</html>

We’ll test James’ code with the QUnit framework. qunit.css will style the test suite page while qunit.js will actually perform the tests.

The core jQuery library is here as well: we’re pointing to the one in the root of the build folder. We’ll use it to help us with some DOM-related tests.

Next is the previously-mentioned app.js which contains the code getting tested. QUnit knows to only test app.js because of its data-cover attribute.

The empty <script> tag is where we’ll write our tests.

Finally, we have two <div> tags: <div id="qunit" /> and <div id="qunit-fixture" />. QUnit’s test results load into <div id="qunit" /> and we’ll use <div id="qunit-fixture" /> to test DOM manipulation.

A quick note about QUnit

QUnit isn’t the only JavaScript unit testing framework and you should review other JS unit testing frameworks at some point. But if you want to learn JavaScript unit testing from the beginning, I think QUnit is best for that.

QUnit has a small API with easy-to-read documentation. This is because it’s maintained by the jQuery team, which is well-known for writing easy-to-read API documentation.

Also, QUnit works great without command line tools like Grunt and Gulp when compared to other testing frameworks. It can run with those tools and you should run unit testing from the CLI eventually.

But to learn JavaScript unit testing from the beginning, running tests in a browser and outside of CLI tooling is fine. Onto the tests…

Test-Driven Development (TDD)

We’ll test this code using Test-Driven Development (TDD), meaning we’ll write our code in four steps:

  1. write a test.
  2. make sure that test fails.
  3. write code to make the test pass.
  4. refactor code if needed.

We’re not doing full-on TDD: we’re using James’ pre-written functions instead of writing tests first and code next. But we’ll add new functions as well as refactor some existings ones…we’ll do enough to understand TDD.

Review James’ code

James’ first FP example used composition and created a small function that returned another small function. He did this by passing one function as a parameter to another:


// A function that gets returned
var log = function(someVariable) {
  console.log(someVariable);
  return someVariable;
}

// A function that takes another function as a parameter
var doSomething = function(thing) {
  thing();
}

// Another function that gets returned and executes 'log()'
var sayBigDeal = function() {
  var message = "I'm kind of a big deal";
  log(message);
}

// All this in action
doSomething(sayBigDeal); // logs 'I’m kind of a big deal'

We’ll use TDD to make log() work but will also update it as follows:

  • log() should only accept a string type as a parameter.
  • the string should have at least one character.
  • Make sure console errors get displayed if these two things don’t happen.

The first failing test

We’ll create a failing test that checks to see if log() returns a string with at least one character:


<!-- test/tests.html -->
<script>

QUnit.test('"log()" should return a string with at least 1 character', function(assert) {

  var myString = 'a string';

  assert.equal(log(myString), 'a string', 'the string was returned successfully!');

  assert.equal(myString.length >= 1, true, 'the string has at least 1 character!');

});

</script>

We create a test with QUnit’s test method. test takes two parameters: the test description and a callback function that runs the test.

The test description is "'log()' should return a string with at least 1 character" and the callback takes a parameter called assert. And assert is THE most important term in unit testing.

assert refers to “assertion” and in unit testing, an assertion is something that our tests always expect to be true. assert points to a QUnit object with methods we can use in our tests.

Our callback contains a variable called myString. This is a “mock,” which is dummy data for our test.

Finally, the callback uses one of the assert methods we have access to: assert.equal(). And it uses it twice.

The first assertions

assert.equal() takes three parameters:

  1. the actual behavior: the code is being tested.
  2. the expected behavior: what we expect the test result to be (or, what we’re “asserting”).
  3. what message should display if the test passes.

For the first assertion, we are:

  1. executing the log() function by running log(myString).
  2. expecting that the function’s returned result will be "a string".
  3. if the test passes, we’ll get a message saying "the string was returned successfully!"

For the next assertion, we are:

  1. claiming that the string’s length is greater than or equal to 1, and it “actually” is.
  2. expecting the first point to be true with the help of a standard Boolean true check.
  3. if the test passes, we’ll get a message saying "the string has at least 1 character!"

See if the tests failed

Our QUnit test suite shows failing tests when we load test/tests.html in a browser…
First failing test image for the learn JavaScript unit testing post
We can make the test pass by adding James’ original log() code to app.js. And note the ES5 "use strict" statement: it will be important later on…


// app.js
'use strict';

var log = function(someVariable) {
  console.log(someVariable);
  return someVariable;
}

…and we’ll see that both tests pass when we go back to our test suite and click on the description.

First passing test image for the learn JavaScript unit testing post

And if we run log() in scripts.js, a console message will appear when we go to our web page:


// scripts.js
log("I'm kind of a big deal"); // logs the first "I'm kind of a big deal"

Test for error messages with assert.throws()

log() should also throw an error message to the console if its parameter isn’t a string with at least one character. We’ll throw those messages using JavaScript’s Error object, creating this functionality with TDD.

QUnit’s assert functionality has a throws() method that tests if your custom error messages get thrown correctly. We’ll use it to create the failing tests for this “throw an error message” functionality.

The failing tests go at the bottom of the script tag on our test suite page:


<!-- test/tests.html-->
<script>
...
QUnit.test('"log()" should throw an error if no parameter is passed or if the parameter is not a string', function(assert) {

  assert.throws(function () {
    log();
  }, 'an error was thrown because no parameters are passed to "log()"');
  
  assert.throws(function () {
    log('');
  }, 'an error was thrown because an empty string is the parameter');

  assert.throws(function () {
    log(null);
  }, 'an error was thrown because "null" is the parameter');

  assert.throws(function () {
    log(undefined);
  }, 'an error was thrown because "undefined" is the parameter');

  assert.throws(function () {
    log(function(){});
  }, 'an error was thrown because a function is the parameter');

  assert.throws(function () {
    log(new Symbol('a symbol'));
  }, 'an error was thrown because an ES2015 symbol is the parameter"');
  ...
  // shortened so it's more readable
});
</script>

Like before, we create a failing QUnit test with a description and a callback that runs the test. We create tests that assume that the parameter doesn’t exist for whatever reason: an empty parameter, an empty string, null and undefined.

Next, we test if either a function or an ES6 Symbol is being passed. This post’s source code also tests for numbers, arrays, objects, Booleans and regular expressions….I left them out here to keep things more readable.

This produces failing tests:
Second failing test image for the learn JavaScript unit testing post
The tests pass when in true TDD form, we refactor log():


// app.js
...
var log = function(someVariable) {
  if((typeof someVariable !== 'string') || (someVariable.length <= 0)) {
    throw new Error('expecting a string with at least one character');
  } else {
    console.log(someVariable);
    return someVariable;
  }
};

And we go back and check our tests…
Second passing test image for the learn JavaScript unit testing post
The test suite confirms that log() throws errors when its parameter is not a string with at least one character. So if we update the log() call in scripts.js to look like this…


// scripts.js
...
log(""); // logs 'Uncaught Error: expecting a string with at least one character'

…an error message will appear in the console when go to index.html.

Make sure to reset log(""); to log("I’m kind of a big deal"); in scripts.js before proceeding.

About code coverage

Code coverage is the analysis of how much of your code is getting tested. It’s almost always measured as a percentage.

Should you always go for 100% code coverage when unit testing? Maybe: search the web and you’ll find a million different answers to the question.

I say do your research and make you’re own decision, but we’re going for 100% coverage in this small example. And in JS unit testing, the most popular code coverage tool is Blanket.js.

We’ll add Blanket.js between jquery.js and app.js in test/tests.html:


<!-- test/tests.html-->
...
<script src="../jquery.js"></script>

<script src="blanket.min.js"></script>

<script src="../app.js" data-cover></script>
...

What code coverage looks like in the test suite

Like we did with log(), we want doSomething() to do type-checking. So we’ll refactor James’ original FP code and add it to the bottom of app.js:


// app.js
...
var doSomething = function(someFunction) {
  if(!$.isFunction(someFunction)) {
    throw new Error("doSomething's parameter must be a function");
  } else {
    return someFunction();
  }
};

We’re using jQuery $.isFunction() to check if the passed parameter is a function. Next, we’ll refresh our test suite and check the “Enable coverage” checkbox that now appears at the top.

The Blanket.js interface will appear: click on the link to app.js to see how much code is getting coverage:

Code coverage image for the learn JavaScript unit testing post

Whatever’s highlighted in green is being tested whatever’s highlighted in red is not. And as we see, doSomething() isn’t being test at all.

We can add the following to the bottom of the script tag in our test suite page….


<!-- test/tests.html-->
<script>
  ...
  QUnit.test('"doSomething()" should return a function', function(assert) {

    var myFunc = function(returnFunc){};
    assert.equal(doSomething(myFunc), myFunc(), 'the function was returned successfully!');

  });
</script>

And if we look at the test suite coverage info, we see we’re testing more code that passes unit tests.
Second code coverage image for the learn JavaScript unit testing post
We’re not testing if our type-checking works like we did for the log() function. We can fix this by adding type checks again at the bottom of the test suite’s script tag:


<!-- test/tests.html-->
<script>
  ...
  QUnit.test("'doSomething()' should throw an error if no parameter is passed or if the parameter is not a function", function(assert) {

  assert.throws(function () {
    doSomething();
  }, 'an error was thrown because no parameters are passed to "doSomething()"');

  assert.throws(function () {
    doSomething("");
  }, 'an error was thrown because an empty string is the parameter');

  assert.throws(function () {
    doSomething('function');
  }, 'an error was thrown because a string is the parameter"');

  assert.throws(function () {
    doSomething(null);
  }, 'an error was thrown because "null" is the parameter');

  assert.throws(function () {
    doSomething(undefined);
  }, 'an error was thrown because "undefined" is the parameter');

  assert.throws(function () {
    doSomething(new Symbol("a symbol"));
  }, 'an error was thrown because an ES2015 symbol is the parameter"');

  assert.throws(function () {
    doSomething(345345);
  }, 'an error was thrown because an number is the parameter');
  ...
  // shortened so it's more readable
</script>

(Side note: Running assert.throws() in two different tests isn’t DRY. I couldn’t find a way to DRY everything like I wanted, so this is something I’ll research in the future. Feel free to let me know if you have a cool way to do it.)

The tests pass with 100% code coverage:
Third code coverage image for the learn JavaScript unit testing post
We can now implement James’ final code for this at the bottom of scripts.js


// scripts.js
...
var sayBigDeal = function() {
  var message = "I'm kind of a big deal";
  log(message);
}

doSomething(sayBigDeal); // logs the second "I'm kind of a big deal"

Testing more functional programming composition

James Sinclair’s FP post demonstrated composition with another function that built a carousel:


function initialiseCarousel(id, frequency) {
  var el = document.getElementById(id);
  var slider = new Carousel(el, frequency);
  slider.init();
  return slider;
}

initialiseCarousel('main-carousel', 3000);

A lot going on here:

  • initialiseCarousel() takes an id and frequency parameter.
  • id is in the el variable, which finds an element on the page.
  • the el variable and frequency parameter get passed to a slider variable, which is an instance of a constructor function called Carousel().
  • slider‘s two parameters, el and frequency, respectively define which element is a carousel and how many times it spins.
  • instances of Carousel(), like slider, have access to an init() method.
  • slider is explicitly returned.
  • when initialiseCarousel() runs, it places a new carousel in a main-carousel page element and gives it a duration of 3000, which I assume represents milliseconds.

In our quest to learn JavaScript unit testing, we’ll test Carousel() and initialiseCarousel() separately. And since James’ tutorial didn’t create Carousel(), it’s an excellent chance to create it with TDD!

Unit test a constructor function

Since Carousel() is a constructor function, we can attach its parameters to this, then return this itself. So we’ll place a failing unit test for this at the bottom of the script tag in our test suite:


<!-- test/tests.html-->
<script>
  ...
  QUnit.test('"Carousel()" should return a string and a number', function(assert) {
      
    var someString = 'some-element';
    var someNumber =  4545935234


    assert.ok(new Carousel(someString, someNumber), 'a string and a number were returned!');
  });
</script>

Carousel‘s two paramters should be a string and a number. So we’ll create two variables called someString and someNumber and pass them to new Carousel() in our test.

We’re using QUnit’s assert.ok() method, which really just checks if our actual value, new Carousel(str, num), exists. I don’t know if this is the strongest unit test in the world: I just want you to be aware that assert.ok is an option.

Also, take note that we’re using the new keyword in our assertion. This goes back to our using 'use strict' and how function’s define their scope in that scenario.

Doing strict mode and not using new like this in your tests leads to bugs, so be sure to always use new in these cases. Read the answer to the Stack Overflow question I asked about this to learn more.

We’ll confirm that the test fails…
Carousel failing image for the learn JavaScript unit testing post
And add the following code to app.js:


function Carousel(getElement, spinDuration) {
  this.getElement = getElement;
  this.spinDuration = spinDuration || 3000;
  if(this.getElement === undefined) {
    throw new Error('Carousel needs to know what element to load into');
  } else {
    return this;
  }
}

Carousel() receives a getElement and spinDuration parameter. Their values will eventually get passed around to initialiseCarousel() when new Carousel() runs inside it.

We’re letting spinDuration be an optional parameter by giving it a default value. If it’s left blank in a Carousel() instance, it will automatically be set to 3000.

But we’re still expecting the getElement parameter: otherwise, our code won’t know where to place the carousel. So we’ll throw a console error if that’s left blank.

(Side note: We’re not going to throw errors if the wrong types get passed. We’ve already done it twice and understand how it works but as a challenge, try adding them to this test on your own.)

The test passes now. But our code coverage indicates that we didn’t test our thrown error functionality:
First carousel code coverage image for the learn JavaScript unit testing post
So we add this test:


<!-- test/tests.html-->
<script>
  ...
  QUnit.test('"Carousel" should throw an error if an element was not passed as a parameter', function(assert) {

    assert.throws(function () {
      new Carousel();
    }, 'an error was because an element was not passed as a parameter');

  });
</script>

And we get 100% coverage on our tests:
Second carousel code coverage image for the learn JavaScript unit testing post
Adding a carousel without parameters to the bottom of scripts.js consequently produces a console error when index.html runs in the browser:


// scripts.js
...
var someCarousel = new Carousel(); // logs "Carousel needs to know what element to load into"

But no errors appear when we pass both parameters…


// scripts.js
...
var someCarousel = new Carousel('carousel-one', 5345); // no console errors

Or even just one element parameter since we have a default value for spinDuration:


// scripts.js
...
var someOtherCarousel = new Carousel('carousel-two'); // no console errorst

The init() method

We’ll just make the carousel’s init() method load text into the carousel page element. We’ll add the following test for this at the bottom of the script tag in the test suite:


<!-- test/tests.html-->
<script>
  ...
  QUnit.test('"Carousel()" should run its init() method and load the proper text', function(assert) {

    var testCarousel = new Carousel('qunit-fixture');
    testCarousel.init();

    assert.equal($('#qunit-fixture').html(), 'The qunit-fixture carousel has started.', 'init() ran and loaded the proper text!');
  });
</script>

We create a new instance of Carousel() called testCarousel and pass qunit-fixture as its single parameter. qunit-fixture points to the standard page element where QUnit loads other elements that need testing.

(Side note: elements that load into qunit-fixture for testing are removed when the tests are done.)

We don’t need to pass a number for the spinDuration parameter. We already gave it a default value in the Carousel() function in app.js, so this test should pass without it.

init() should place a custom message in <div id="qunit-fixture" /> that says "The qunit-fixture carousel has started.". Then we’ll use jQuery’s html() function to look in the qunit-fixture and see if its copy matches our message.

If the copy matches, our QUnit test will say "a slider was returned!" But for now, we have a failing test:

Init failing test image for the learn JavaScript unit testing post
Adding this code to the bottom of app.js will get things to work:


// app.js
...
Carousel.prototype.init = function() {
  var getCarousel = document.getElementById(this.getElement);
  getCarousel.innerHTML = 'The ' + this.getElement + ' carousel has started.';
};

We’ve followed JavaScript best practices and placed init() on Carousel‘s prototype instead of in the Carousel constructor function. It finds the element defined in Carousel using document.getElementById(), which is this.element, and stores it in a getCarousel variable.

Next, init() takes the value of this.element and uses it to build a custom message. The message gets loaded into whatever element getCarousel points to.

As a result, our unit test passes with 100% code coverage:
Init passing test image for the learn JavaScript unit testing post
And if we run init() two times at the bottom of scripts.js


// scripts.js
var someCarousel = new Carousel('carousel-one', 5435);
someCarousel.init(); // show “The carousel-one carousel has started” on index.html

var someOtherCarousel = new Carousel('carousel-two');
someOtherCarousel.init(); // show “The carousel-two carousel has started” on index.html

And two text blocks will show up in the carousel-one and carousel-two page elements when index.html runs in the browser.

Unit test the returned function

In James’ example, the returning function, initialiseCarousel() was expected to return a new instance of Carousel(). A failing test for that looks like this:


<!-- test/tests.html-->
<script>
  ...
  QUnit.test('"initialiseCarousel()" should return a new instance of Carousel()', function(assert) {
      
    var testCarouselInstance = initialiseCarousel('qunit-fixture', 3000);
    var isCarouselInstance = testCarouselInstance instanceof Carousel;
    assert.ok(isCarouselInstance, 'a new instance of Carousel() was returned!');

  });
</script>

We’re testing with assert.ok() again. The test has a description and a callback as usual and the callback has two variables:

  1. testCarouselInstance stores an invocation of initialiseCarousel() that creates a carousel in <div id="qunit-fixture" /> with a 3000 millisecond duration.
  2. isCarouselInstance stores a test for if testCarouselInstance is actually an instance of Carousel using instanceof.

So we have a failing test right now…
initialiseCarousel failing test image for the learn JavaScript unit testing post
Then we get to pass by adding James’ original to app.js


//app.js
...
function initialiseCarousel(id, frequency) {
  var el = document.getElementById(id);
  var slider = new Carousel(el, frequency);
  slider.init();
  return slider;
}

And the test passes…
initialiseCarousel passing test image for the learn JavaScript unit testing post
And since our tests confirm that a new Carousel() instance exists and we’re explicitly returning that instance (slider), we can agree that our test is accurate.

We can now invoke initialiseCarousel() in scripts.js to get it working in live code.


// scripts.js
...
var testCarousel = initialiseCarousel('main-carousel', 3000); // "The main-carousel carousel has started" displays on the page

index.html displays “The main-carousel carousel has started” when it runs in the browser. And since we’re already getting the DOM element in Carousel(), we can refactor initialiseCarousel() and removing that functionality from it:


//app.js
...
function initialiseCarousel(id, frequency) {
  var slider = new Carousel(id, frequency);
  slider.init();
  return slider;
}

Note that id replaces el in slider‘s parameter.

Bringing it altogether

James’ last example performs roughly the same functionality as the others:


function addMagic(id, effect) {
  var element = document.getElementById(id);
  element.className += ' magic';
  effect(element);
}

addMagic('unicorn', spin);
addMagic('fairy', sparkle);
addMagic('kitten', rainbow);

addMagic() takes id and effect as parameters. id is passed to the element variable inside addMagic(), where element references a page element.

element gets a class named magic added to it. It also has an effect function invoked inside it, hence, the effect parameter.

effect can be either spin, sparkle or rainbow. Like before, we’ll update these functions to load text inside of a page element.

We can unit test all this using everything we’ve learned up to this point. Our first failing test looks like this:


<!-- test/tests.html-->
<script>
  ...
  QUnit.test('addMagic() should return a function and add a "magic" class to the target element', function(assert) {

    function returnFunc(){}
    addMagic('qunit-fixture', returnFunc);

    assert.equal(typeof returnFunc, 'function', 'the function was returned successfully!');
    assert.equal($('#qunit-fixture').hasClass('magic'), true, 'The targeted element has a class named "magic" !');

  });;
</script>

The test invokes addMagic() to find the qunit-fixture page element and return a function named returnFunc. We’ve tested for returned functions before only this time, we’re testing for this using typeof in our first assert.

The second assert tests if the magic class was dynamically added. It uses jQuery’s hasClass functionality to do so.

The test structure for the three effects is a little different;


<!-- test/tests.html-->
<script>
  ...
  QUnit.module('addMagic() effect tests', function() {
    QUnit.test('spin() should load "spinning..." into its targeted element', function(assert) {
      addMagic('qunit-fixture', spin);
      assert.equal($('#qunit-fixture').html(), 'spinning...', 'The targeted element contains text that says "spinning..."');
    });

    QUnit.test('sparkle() should load "sparkling..." into its targeted element', function(assert) {
      addMagic('qunit-fixture', sparkle);
      assert.equal($('#qunit-fixture').html(), 'sparkling...', 'The targeted element contains text that says "sparkling..."');
    });

    QUnit.test('rainbow() should load "rainbowing..." into its targeted element', function(assert) {
      addMagic('qunit-fixture', rainbow);
      assert.equal($('#qunit-fixture').html(), 'rainbowing...', 'The targeted element contains text that says "rainbowing..."');
    });

  });
</script>

Each test passes an effect to addMagic as a parameter. Since each effect places text inside a page element, we’re using jQuery’s html() function again to look for the existence of that text.

This time though, we’re wrapping all these tests inside QUnit.module(). This groups these three tests and makes them stand out a little in our test suite, which is a bit more readable.

So, we have our failing tests now…
addMagic failing test image for the learn JavaScript unit testing post
Adding this code to app.js makes the tests pass…


// app.js
...
function addMagic(id, effect) {
  if(!id || !effect) {
    throw new Error('addMagic() needs an id and effect parameter');
  } else {
    var element = document.getElementById(id);
    element.className += ' magic';
    return effect(element);
  }
}

function spin(getElement){
  getElement.innerHTML = 'spinning...';
}

function sparkle(getElement){
  getElement.innerHTML = 'sparkling...';
}

function rainbow(getElement){
  getElement.innerHTML = 'rainbowing...';
}

We’ve slightly adjusted addMagic() where it throws an error if either of its parameters aren’t passed. We’ve also explicitly returned the effect invocation.

Next, we create our three very simple effect functions. Again, they just load some text in whatever page element is defined by their getElement parameters.

We look at our test suite, including the code coverage…
First addMagic code coverage test image for the learn JavaScript unit testing post
And we see that the tests pass, but not with 100% code coverage.
Second addMagic code coverage test image for the learn JavaScript unit testing post
This is due to our not testing addMagic‘s error throwing functionality. Adding a couple of assert.throws() tests will fix this…


<!-- test/tests.html-->
<script>
  ...
  QUnit.test('"addMagic()" should throw an error if less than 2 parameters are passed', function(assert) {

    assert.throws(function () {
      addMagic();
    }, 'an error was thrown because no parameters were passed to "addMagic()"');

    assert.throws(function() {
      addMagic(spin);
    }, 'an error was thrown because only one parameter was passed to "addMagic()"');

  });
</script>

And looking at our test suite and code coverage confirms this. Note that the grouped “addMagic() effect tests” are moved below these new tests even though the grouped tests are above them in the suite code.
Second addMagic throw error test image for the learn JavaScript unit testing post
So we can add this code to scripts.js


// scripts.js
...
addMagic('unicorn', spin);
addMagic('fairy', sparkle);
addMagic('kitten', rainbow);

And copy loads into elements already on the page.

Further reading

I’ll start with the JS unit test stuff first…

  • Test-Driven JavaScript Development & Testable JavaScript: These two books stand from the others in the JavaScript unit testing worls. The second one is easier to read, but the first one is the most thorough book on the subject. You may want to read Testable first but make sure to read Test-Driven at some point.
  • TDD Terminology Simplified: Truthfully? This is list of terms can be applied to unit testing overall and not just TDD. And if you’re concerned that I didn’t cover JavaScript unit testing top to bottom, this list is the next step. For example: we saw earlier that elements which load into qunit-fixture for testing are removed when the tests are done. Keep that in mind, then go to this link and read about “setups” and “teardowns.”
  • 5 Questions Every Unit Test Must Answer: A general primer from Eric Elliot on JavaScript unit testing with some smart best practices. Check out how he suggests using equal tests only for a week and his pattern for creating actual/expected tests in constants.
  • Writing Testable Javascript: A nice high-level view by Rebecca Murphey of how to write FP-like code that’s easy to test…also watch her conference talk of the same name.
  • JS Assessment & Codewars: The first one is a CLI-powered test (also by Rebecca Murphey) and the second one’s an app. Each one requires you to write code that passes tests before moving forward. Codewars has a badge-like point system that’s pretty cool.

Here’s some functional programming stuff…

Conclusion

Any developer can learn JavaScript unit testing. But not until they understand that they can never again place 50 lines of code in a single $(document).ready() block.

They must realize that using functional programming to create small, testable functions will make them an awesome JS unit tester. And a better developer as well!!!


Leave a Reply

Your email address will not be published. Required fields are marked *