This post starts with a lot of useful background information, and then walks you through implementing your own basic test framework. You should read through the background and implement your own test framework - but if you’re just itching to code, do a quick review of the prerequisites then jump to Implementing a Learners' Level Test Framework.

Prerequisites

You really don’t need a lot of prerequisite knowledge to follow along with this lesson.

Web Console

You do need to be able to use your browser’s web console and scratchpad. If you aren’t familiar with your browser’s console and scratchpad, wait for me to add a video version of this post.

JavaScript

you need to be minimally familiar with the following concepts.

// comments (1)

var variable = 'defined'; // var and the idea of variables (2)

function recipe (parameters) {
	/* functions,
		specifically:
		function declaration (3)
		and function parameters (4)
	*/
	return true; // how to return values from a function (5)
}

If you don’t know these things, check out:

JavaScript that you will pick up en route

As we proceed, we’ll actually touch on a lot of other aspects of JS, including if statements, equality, objects. But, you do not need to know anything about these to follow along. I’ll either explicitly explain what I’m doing or I’ll assume that the context makes it clear what’s going on (where I’ve done that, I’ve also provided links to relevant documentation, so that if you want to dive into it more you are on a good diving board).

Background

What is TDD?

TDD stands for Test Driven Development. An extremely related concept is Behavior Driven Development (BDD). The idea for both is pretty simple - when developing code, you should know and test the outcome of your code before you actually write the code. That may sound weird, but bear with me for a moment.

Imagine you’re teaching a class of kids how to do subtraction for the very first time. After giving them the introduction, you write "3 - 1 = " up on the board, then have each kid write their answer on a sheet of paper and hold it up. Most kids hold up a sheet with the number "2" in it, but a few hold up a sheet with the number "4" on it. You now have a snapshot of the whole class and which students likely don’t quite understand the concept. TDD is kind of like this, but for you and your code.

Test Pseudo-code and Example

Our primary goal is to write a basic test first. A basic test looks something like like:

var is = 'the code to be run';
var expect = 'the result I expect back';
var msg = 'a name for the test';

test(is, expect, msg);

Image we have to write an add function, that takes two parameters and returns their sum. Using the example above, what might such a test look like?

Well it could be like anyone of the following examples, and many others!

test( add(1,1), 2, '1+1 = 2' );
test( add(42,0), 22, '42+0 = 0' );
test( add(42,0), 22, 'positive and zero' );
test( add(3.14, 3.14), 6.28, 'pi to tau' );
test( add(-100, -50), -50, 'negative and negative' );

If you notice, I’ve made a call to a test function, but that function doesn’t exist - yet! We’ll come back to this in just a moment, but first we’re going to take a really quick detour into some terminology and styles.

TDD Frameworks and Testing Types

There are lots of TDD frameworks and services, including Jasmine, Mocha, Chai, Selenium, PhantomJS, Protractor, Nightwatch, Dalek, BrowserStack, SauceLabs, and Testingbot.

Please don’t spend time trying to get these up and running or using these until:

  1. You’re more comfortable with JS,

  2. You’re working with other people on a project, or

  3. You’re working on larger projects.

At the end of this post, I’ve provided additional information on some of these frameworks and services.

Basic Concepts

The following are some really common terms in the TDD realm that you should at least recognize.

Test Types

Unit Tests

Unit Tests test individual JavaScript functions. For this exercise, we’re going to be working exclusively with unit tests.

Integration Tests

Integration Tests test groups and the interrelation of JavaScript functions.

End-to-End Tests

End-to-End Tests test specific functionality from the front end to the back and back again.

Assertions

Assertion Styles

Assertion styles simply specify the lingo to be used in tests. There are three primary styles: assert, expect, should.

  • assert style example assert.equal( add(1,1), 2, '1+1 = 2' );

  • expect style example expect.( add(1,1), '1+1 = 2' ).to.equal( 2 );

  • should style example add(1,1).should.equal( 2 );

We’re going to work up to a slightly different style, specifically Test.assertEquals( add(1,1), 2, '1+1 = 2' );.

The important thing to notice is that while there are different styles, they all include the same three elements: is, should, and msg. Everything else is just "semantic sugar".

Red, Green, Refactor

One last thing before we dive into the code.

In the realm of TDD, the goal is to initially fail. You read that correctly, we want to fail first. This is frequently referred to as red (and most frameworks will show you a failing test result in red). This is because we want to make sure that our test is actually testing what we think it is.

Of course, once we write the code, we want our test to pass. This is referred to as green (and if you think most frameworks will show you the result in green, you are correctomundo). We now have reasonable confidence that our test and our code do what we told them to do. Just beware - what you told them to do may not be what you actually want them to do. As you code more, you’ll understand this caveat.

Once you have the test and code working as stated, then you can edit your code to be more clear, more concise, more precise, etc. This is known as "refactoring" the code. One of the awesome results of TDD, is that you can refactor your code in the confidence that if you break anything that you’ve written a test for, you will be notified of what was broken by a failing test!

Implementing a Learners' Level Test Framework

Okay, now for the fun part!

We’re going to iterate through writing our own test framework. It will of course be very simple, but by writing it yourself, you can (a) easily test your own functions without being dependent on a specific test framework, (b) customize it to your hearts extent, and (c) expand it as your needs grow.

Version 0 - Red

Since I’m clearly advocating TDD, we’re actually going to TDD our own test framework!

To do this, we’re going to write some really basic calls to our non-existent test framework.

These tests are not going to work like normal tests.

We’re specifically going to write two tests: one testing a passing condition and the second testing a failing condition. In other words, we will want the second test to fail!

We’re going to start with the test style from the pseudo-code above.

test( is, should, msg );

In our first test, we’re going to ensure that the same value results in a passing test.

test( 42, 42, 'a passing condition' );

In our second test, we’re going to ensure that different values result in a failing test.

test( 3.14, 6.28, 'a failing condition' );

Open your browser’s scratchpad and enter your meta-tests. Your final version 0 meta-test should look something like:

// my meta-tests
test( 42, 42, 'my passing condition' );
test( 3.14, 6.28, 'my failing condition' );

With your tests in your scratchpad, go ahead and run it. You should get something to the effect of:

/*
Exception: ReferenceError: test is not defined
@Scratchpad/4:2:1
*/

If so - AWESOME! This is your very first "red"! WOOT! WOOT!

If your result does not include something to the effect of test is not defined - STOP - go check and double check everything until you’re getting this type of error.

Version 1 - Green

Now it’s time to go green.

First, review our error message.

/*
Exception: ReferenceError: test is not defined
@Scratchpad/4:2:1
*/

The crux of the error is the test is not defined bit. Knowing that, we’re going to start by defining test. We could just do a var test, but test is going to be a function, so instead we’re going to do this:

function test() {

}

Now if you run your code, what do you get?

I got nothing at all. No error, no console.log messages, nothing, nadda, nill.

Why might this be?

Well, we call test twice, and presumably test ran twice, but test itself does nothing, nadda, nill.

So let’s make test do something. How about outputting a simple message?

function test() {
  console.log('is test doing what we tell it to do');
}

If you run that, you should get back your message twice!

In other words, test is doing exactly what we tell it to at this point.

So, let’s tell it to compare an is with a should and log the result.

function test(is, should) {
  console.log (is === should);
}

If you run your code now, you should get one true and one false - that’s almost exactly what we want. In other words, we’re on the right track! It’s close enough that you could actually use it just like this if you wanted!

But it’s much more clear to have an actual "pass" or "fail". We can accomplish this several different ways. For reasons that may become clear, I’m going to steer us toward an if approach. So, let’s implement a basic if check then log either "pass" or "fail".

This is how I implemented my basic if check with the corresponding logs:

function test(is, should) {
  if (is === should) { (1)
    console.log('pass');
    return; (2)
  }
  console.log('fail');
}
1 If you aren’t familar with JS comparison operators, the triple equal is a "strict equality" operator. It returns true only if the items being compared are exactly equal. There are many other types of comparisons. Most test frameworks will actually incorporate different types of equality and comparisons. For this exercise, we’re only going to use strict equality. But remember - this is your library, so when we’re done feel free to add some some additional comparisons!
2 If you look closely, I added a return and didn’t use an else. The return inside the if causes any passing test to exit at that point. As I understand it, this improves performance –ever so slightly–. It is also a stylistic choice.

If you haven’t figured it out yet - We’re GREEN!!!! WOOT! WOOT! It’s time to do a happy dance, or at least a high five, thumbs up, fist bump with someone nearby!

Version 2 - Refactor

As you’re using your test library, there are a handful of things that you’re likely to want. We’ll refactor to add these things in.

Message / Test Name

First, let’s add our test’s message to the output, but only if we fail. We can do this by using the + operator to concatenate msg to the fail’s log. While we’re at it, let’s make the fail message a bit more prominent and add a new line before each test’s output.

My code looks like this now:

function test(is, should, msg) {
  if (is === should) {
    console.log('\npass');
    return;
  }
  console.log('\nFAIL: ' + msg);
}

Don’t forget to run your code and make sure you get your expected output! For me, that’s:

pass

FAIL: my failing condition

is vs. should

If a test fails, it’s also useful to know what we got compared to what we expected. So let’s add that to our fail message.

My version looks like this:

function test(is, should, msg) {
  if (is === should) {
    console.log('\npass');
    return;
  }
  console.log('\nFAIL: ' + msg + '\nexpected "' + should + '" but got "' + is + '"');
}

Which outputs like this:

pass

FAIL: my failing condition
expected "6.28" but got "3.14"

Jazz it up a bit

Let me show you a cool thing about most modern browsers, that’s particularly relevant for our output:

function test(is, should, msg) {
  if (is === should) {
    console.log('%c\npass', 'color: green');
    return;
  }
  console.log('%c\nFAIL: ' + msg + '\nexpected "' + should + '" but got "' + is + '"', 'color: red');
}

Without running that code, what do you think is going to happen?

If you guessed that it colors passing logs green and failing logs red - great guess!

If you wanted to leave your test function right here, it’s pretty dang useful as it is; but let’s bump it up just a notch! ;-)

Version 3 - Object-ifying your test

I highly recommend working through Codewar’s code katas. One of the great things about the lower level katas is that most come with some visible tests. BUT frequently there are important tests that aren’t visible, and as you move up through the ranks kata authors provide fewer and fewer tests. So, it’s a great place to get in the habit of writing your own tests! Codewars has their own test framework. If you check out their documentation, you’ll notice that our function is most like their Test.assertEquals.

Don’t fret if that function call looks a bit strange. The Test part indicates that it is an object functioning as a "pseudo-class" and the .assertEquals is simply a function within that pseudo-class.

But, before we go any further, let’s change our meta-tests to use our new function’s target syntax. We’ll do that by changing test to Test.assertEquals:

// my meta-tests
Test.assertEquals( 42, 42, 'my passing condition');
Test.assertEquals( 3.14, 6.28, 'my failing condition');

Don’t forget to run your revised code!

You should get a Test is not defined error, which should feel awfully familiar!

This time, we are going to use a var. Specifically, we’re going to create a new function using a "function statement" before our current test function.

My code looks like this now:

// my actual Test object
var Test = function() {

};

Go ahead and run your code - I’ll wait ;-)

If your test calls come before your new Test function, then you should have gotten the same "Test is not defined" error! But do not sweat this - just move your test calls after your new function, so that your code looks something like this:

// my actual Test class
var Test = function() {};

function test(is, should, msg) {
  if (is === should) {
    console.log('%c\npass', 'color: green');
    return;
  }
  console.log('%c\nFAIL: ' + msg + '\nexpected "' + should + '" but got "' + is + '"', 'color: red');
}

// my meta-tests
Test( 42, 42, 'my passing condition');
Test( 3.14, 6.28, 'my failing condition');

So why were you able to run test after your test calls, but you couldn’t run Test after your test calls? Well, that’s a really awesome question! I’m going to say that one more time just for emphasis - that’s a really awesome question!

Unfortunately, the answer is kind of a rabbit hole –a great rabbit hole– but a rabbit hole nonetheless. Since I don’t want to go down that rabbit hole in this article, I going to highly recommend that you Dive Deep into JavaScript sometime soon, but preferably after you’re done working through this post.

When you re-run your scratchpad, you should experience more than a little bit of déjà vu as you read the "Test.assertEquals is not defined" error. But before you despair too much, note that error actually includes the assertEquals part, which means it’s actually a different error.

Before we address that, let’s play around for just a bit. Specifically, add the following after your empty Test function and then run your code.

Test.yo = function() {
  console.log("We're about to rock our Tests!");
};

Test.yo();

Before I go any further, try to work through what you think the code is, and then try to modify your test function so that your tests work.

If your code doesn’t work and you can’t figure out the next step based on the Test.yo code, then go ahead and review my final, working code:

// my actual Test class
var Test = function() {};

// my actual assertEquals Test method
Test.assertEquals = function(is, should, msg) {
  if (is === should) {
    console.log('%c\npass', 'color: green');
    return;
  }
  console.log('%c\nFAIL: ' + msg + '\nexpected "' + should + '" but got "' + is + '"', 'color: red');
}

// my meta-tests
Test.assertEquals( 42, 42, 'my passing condition');
Test.assertEquals( 3.14, 6.28, 'my failing condition');

That’s mostly it. You can now write lot’s of strict equality tests and get useful information back. Even better, you should be able to:

  • Write a super quick, super simple test function in JavaScript!

  • Modify your own version of this strict equality test!

  • Add new tests to your Test object to your heart’s content!

Consider this —your very first code wizard’s wand— as my gift to you. Please enjoy doing awesome magic with it!

Next Steps

Add all the tests!

As I’ve mentioned, the library we just wrote only evaluates strict equality.

Not only are there other things that can be evaluated (e.g. greater than, less than, includes, etc.), strict equality does not consider different arrays or objects to be "equal", even if they are exact same value! You probably want to incorporate some type of array and object comparison into your own library at some point. If that does not make sense - it’s not the right time for you to do it - and that’s perfectly fine! When you start working with arrays and objects, just remember that this test framework won’t do what you want. But also remember that you have the power to add that functionality to your test framework! (And it’s a wonderful exercise at that point! ;-) )

Framework and Service Overview

I’m certainly no expert on TDD frameworks or services –heck, I still struggle to get them set up in my own projects :-( – but I have experimented with some, and read and heard about even more.

Here’s what I’ve picked up so far, just know it is by no means comprehensive, but hopefully it helps you on your JavaScript adventures!

Mocha and Jasmine

The two unit and functional testing frameworks I’ve encountered. While I love Jasmine’s documentation, I have a slight preference for Mocha.

Chai

The assertion library that I’ve used.

Selenium

Specifically, Selenium WebDriver, the underlying technology for most of the headless browsers and end-to-end test frameworks. I have a hate-love-hate relationship with it. It’s been a pain to get up and running or updating, but when it’s working it’s practically invisible.

PhantomJS, CasperJS, Zombie.js, DalekJS

These are all headless browsers, which basically means they run end-to-end tests against a theoretical browser. I’ve barely used any of these, but love their names.

Protractor, Intern, and Nightwatch.js

These are all true end-to-end test frameworks. Protractor is strongly associated with Angular; I haven’t used it, but have heard good things. I’ve played a bit with both Intern and Nightwatch.js and have a slight preference for Nightwatch.js.

BrowserStack, SauceLabs, and Testingbot

These are all cross browser testing services. Basically, they are virtual machine farms, that allow you to run your end-to-end tests against a wide range of operating systems and browser versions. I’ve have not tried Testingbot. I have a slight preference for BrowserStack.

comments powered by Disqus