How To

How Do I Test JavaScript? An introduction to Jasmine with some TDD

2 Jul , 2017  

Yeah, you know testing is important, right?

Once upon a time, JavaScript developers could get away with little formal, repeatable testing.

But that was when JavaScript was an optional extra to improve a page.

Now that JavaScript has moved to the centre of web application functionality it’s no longer optional, and testing it isn’t optional either.

As usual in the JavaScript world, we’ve got a bewildering array of options for testing our code.

Today, I’ll look at just one – Jasmine.

We’ll explore using Jasmine to test on the command line and via a web page as we run through a little Test Driven Development (TDD) exercise. TDD can seem weird at first, but it’s worth the effort.

After reading this article (and doing the exercises!) you’ll know how to

  • Install npm and Jasmine
  • Use Jasmine and npm from the command line to execute tests
  • Perform TDD to solve a simple problem

JavaScript TDD with Jasmine Introduction

So, what is Jasmine?

Jasmine is a batteries-included test framework for JavaScript.

The reason I say ‘batteries-included’ is that once you’ve installed Jasmine, you have everything you need to comprehensively test your JavaScript code.

This isn’t always the case – another popular test framework called Mocha takes a component based approach, where we need to install several packages for the same functionality that Jasmine provides.

This isn’t a bad thing – maybe you don’t need everything that Jasmine does, and you’ll be happy with a smaller set of libraries (I actually use Mocha most of the time).

Anyway, we’re using Jasmine today, since it’s the easiest way to get started.

How to define tests in Jasmine

Before we get into actually running tests, here’s a quick introduction on how Jasmine can define a series of tests.

Jasmine uses a terminology that stems from Behaviour Driven Development (BDD). This isn’t an important consideration at this point, but basically means tests are structured like this:

describe 'the thing we want to test'
  describe 'the part of the thing we want to test'
    it 'does something we expect'
      // do the test to find out if the code actually does what we expect

An actual example of code may look like:

describe('Utility to prepare strings for URLs', () => {
  describe('function to prepare a string for an URL', () => {
    it('encodes spaces to %20', () => {
      // And then we do the actual test here.
    });
  });
});

Like I said earlier, we’ll work through more concrete examples later in the article.

How to install Jasmine and set up a new project

Pouring beaker install Jasmine

Jasmine can be installed via npm. (Don’t have NPM installed? See the mini-appendix at the end of this article).

But before we do that, let’s initialize a little project to try Jasmine out with. Those of you familiar with a certain technical interview preparation book will probably recognize the problem we’re working with.

Start by creating a new directory, called urlify. Note that I’m assuming you’re working in a terminal, or bash shell of some kind. Mac people can just use the Terminal app. See here for setting up bash in Win 10.

Anyway, let’s make a directory and initialize it for npm:

mkdir urlify
cd urlify
npm init -y

That npm init -y sets up a basic configuration in a file called package.json for npm. We’ll use npm to install Jasmine, and maybe do some other work for us as well.

If you haven’t had much to do with npm and package.json, don’t worry about it at this stage. They might look weird, but they’re your friends! 🙂

Now can we install Jasmine? I hear you ask.

But of course. It’s as easy as:

npm install jasmine --save-dev

Here we tell npm to install jasmine, and save it to our development configuration. What this means is, that if you ever turn our urlify project into a package that other people can download and install, npm won’t include Jasmine in your urlify package, since Jasmine isn’t required for using the package, just developing it.

But we digress! Next, we need to initialize Jasmine, which is done this way:

node_modules/.bin/jasmine init

(When we do an npm install, packages are installed under node_modules  in your project directory. So we can run node_modules/.bin/package).

Anyway! That jasmine init creates a directory for our test files to go into called spec, and also makes an initial configuration file for Jasmine, which we won’t need to change.

So now the contents of our urlify directory looks like this:

urlify/
  node_modules
  package.json
  spec

Now we need one more directory – a place to put our source code! Let’s make it now. Assuming you’re still in the urlify directory:

mkdir src

And now the urlify directory looks like this:

urlify/
  node_modules
  package.json
  spec
  src

Finally, we need to tell npm to run our test with Jasmine, by updating a line in package.json. So change:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1"
},

To look like this:

"scripts": {
  "test": "jasmine"
},

And now we’re ready to get started.

Key takeaways on installing and setting up Jasmine

  • Jasmine is a test framework
  • Jasmine includes everything you need to perform standard testing
  • We install Jasmine with npm install jasmine –save-dev
  • We initialize Jasmine by executing node_modules/.bin/jasmine init
  • The Jasmine initialization creates a directory called spec for test files, as well as an initial test configuration file.

Getting started with testing

Test Tubes starting JS testing

Time to get our hands dirty and write some…tests!

Yeah, since this is going to be a tiny TDD exercise, the first thing we’ll do is write a test.

Because of course, the whole TDD approach is to define a test and then do the least amount of work to make that test pass. There are of course, nuances, but that’s the nub of it. See Uncle Bob if you’d like to know a lot more about TDD.

So let’s change into the spec directory, and use your favourite text editor to make a new file. You can see that it follows the describe/it/test approach I described earlier:

const urlify = require('../src/urlify.js');

describe('Function urlify(), prepares strings for use in an URL', () => {
  it('is a function', () => {
    expect(typeof urlify).toBe('function');
  });
});

You’ll notice that the very first line includes a file that doesn’t even exist! Never mind, let’s run the test and make our first test pass.

So run the test like this:

npm t

(That t is short for test, which means that npm will run the test command in the scripts section of package.json, which we edited earlier).

Anyway, this is what we’ll get:

> urlify@1.0.0 test ./urlify
> jasmine

module.js:442
throw err;
^

Error: Cannot find module '../src/urlify.js'
[etc]

Unsurprisingly, jasmine is complaining that it can’t find our urlify.js file, which we haven’t created yet.

Let’s create the file (assuming you’re still in the spec directory):

touch ../src/urlify.js

Then run the test again:

npm t

> urlify@1.0.0 test ./urlify
> jasmine

Started
F

Failures:
1) Function urlify(), prepares strings for use in an URL is a function
Message:
Expected 'object' to be 'function'.
Stack:
Error: Expected 'object' to be 'function'.
at Object.it ./urlify/spec/urlify.spec.js:5:27)

1 spec, 1 failure
Finished in 0.008 seconds

Here we can see that Jasmine got a little further, and can complain sensibly about our urlify function not existing.

That F you can see just after Started is what Jasmine prints when a test fails. When a test passes, Jasmine will print a period (.).

So let’s create the function. Edit the urlify.js file in the src directory as follows:

function urlify(s) {
}

module.exports = urlify;

And rerun the test:

npm t

> urlify@1.0.0 test ./urlify
> jasmine

Started
.


1 spec, 0 failures
Finished in 0.005 seconds

And we finally have a passing test, even though it’s barely a test!

What you can expect() with Jasmine testing

JavaScript Jasmine Testing Expect Scales

It’s time to take a closer look at our Jasmine test:

const urlify = require('../src/urlify.js');

describe('Function urlify(), prepares strings for use in an URL', () => {
  it('is a function', () => {
    expect(typeof urlify).toBe('function');
  });
});

As I mentioned before, we’re following the describe/it/test format common to many TDD/BDD frameworks.

The part we haven’t really looked at is the actual test! Here it is again:

expect(typeof urlify).toBe('function');

The traditional way of specifying a test in the BDD world is given…when…then, but you can see that the when…then part is described by the expect…toBe pair.

Jasmine supports lots of tests besides toBe. They include:

  • toEqual
  • toMatch
  • toBeDefined
  • toBeUndefined
  • toBeNull
  • toBeTruthy
  • toBeFalsy
  • toContain
  • toBeGreaterThan
  • toBeLessThan
  • toBeCloseTo
  • toThrow

And any of them can be negated with .not, for example:

expect(typeof urlify).not.toBe('string');

But that’s not a very useful test.

It’s time to do something a bit more relevant to our test, and start using some of the other tests that Jasmine gives us.

Expanding our tests and functionality together

Before we go any further, let’s take a look at what our code is supposed to be doing.

Like I said earlier, this exercise is adapted from an interview preparation book. The intent of the original question was to test a programmer’s ability to process a fixed-length array of characters, and goes like this:

For a given string (array of characters), replace all spaces with ‘%20’. There are extra spaces at the end of the string to make sure the string can accommodate the two extra characters required for each space to become %20, and shouldn’t themselves be replaced. For example:

Input:  'A Test  '
Output: 'A%20Test'

Since that’s an reasonably unlikely scenario in JavaScript, let’s pretend that we’re using JavaScript to replace some old code that had to deal with character arrays1.

Unfortunately, we’re still going to receive input with those extra spaces on the end.

We’ll start by adding a test that makes sure that a string with no spaces is returned unchanged:

const urlify = require('../src/urlify.js');

describe('function urlify(), prepares strings for use in an url', () => {
  it('is a function', () => {
    expect(typeof urlify).tobe('function');
  });

  it('does not change the string "apples"', () => {
    expect(urlify("apples")).toequal("apples");
  });
});

Which predictably yields a test failure:

npm t

> urlify@1.0.0 test ./urlify
> jasmine

Started
.F

Failures:
1) Function urlify(), prepares strings for use in an URL does not change the string "Apples"
Message:
Expected undefined to equal 'Apples'.
Stack:
Error: Expected undefined to equal 'Apples'.
at Object.it (./urlify/spec/urlify.spec.js:10:30)

2 specs, 1 failure
Finished in 0.009 seconds

You can now see how our describe…it pairings elegantly describe our tests: Function urlify(), prepares strings for use in an URL does not change the string “Apples”.

So we’d better modify urlify() to do what we want it to:

function urlify(s) {
  return s;
}

module.exports = urlify;

You might be thinking this is a scandalously superficial implementation, but remember, we’re making the smallest change required to make the test pass.

This is one of the fine lines TDD enthusiasts love to fight about, since there’s also an expectation that the changes we make will satisfy future tests as well.

I prefer to go as minimal as possible, since this stops me from adding extra cruft and creating needlessly complicated code (I compulsively over-engineer).

Hoorah. We’ve added some exceptionally minor functionality by creating a test and then implementing code to make the test pass.

This is the traditional point in the TDD cycle where we’d refactor our code to make it as clean as possible.

But there’s really not much to refactor here, so let’s move on to the next test. It’s time to make sure our code actually changes a space into %20:

it('changes "Apples Oranges  " to "Apples%20Oranges"', () => {
  expect(urlify("Apples Oranges  ")).toEqual("Apples%20Oranges");
});

Which once again fails when we run npm t:

> jasmine

Started
..F

Failures:
1) Function urlify(), prepares strings for use in an URL changes "Apples Oranges  " to "Apples%20Oranges
Message:
Expected 'Apples Oranges  ' to equal 'Apples%20Oranges'.
Stack:
Error: Expected 'Apples Oranges  ' to equal 'Apples%20Oranges'.
at Object.it (./urlify/spec/urlify.spec.js:14:40)

So to fix that, we really need to do two things.

Since we’re working with JavaScript strings, we can use the String object’s replace function to discard the spaces at the end of the string, and turn the other spaces into %20 sequences.

If you’re coding along, go and do that right now! Otherwise, here’s the updated implementation of urlify():

function urlify(s) {
  s = s.replace(/\s*$/, '');
  s = s.replace(/ /g, '%20');
  return s;
}

And now the tests pass:

> jasmine

Started
...


3 specs, 0 failures
Finished in 0.007 seconds

But you know, our tests so far are quite forgiving. What about input like ‘Apples   ‘, or ‘Oranges and Bananas’, or even ‘Apples and Oranges  ‘?

While our code would return correctly transformed strings in these cases, according to our spec those inputs would be invalid.

How forgiving you want to be of external input is up to you, but if some client code is sending invalid input it could be a sign of larger problems.

Personally, I’d want to throw an error, which would hopefully kick off an investigation into what’s going on. Once we know why some client code is sending invalid input we can make a decision to allow it or not.

So here’s a test:

it('throws an error for invalid input "ApplesOranges  "', () => {
  expect(function() { urlify("Apples Oranges") })
    .toThrow(new Error("Invalid input"));
});

Note that in this case, we need to wrap the function we’re testing in another, anonymous function.

Once I’ve run that test to make sure it fails, I use some counters to keep track of transformations:

function urlify(s) {
  const originalLength = s.length;

  s = s.replace(/\s*$/, '');
  const trimmedLength = s.length;
  const trimmedSpaces = originalLength - trimmedLength;

  s = s.replace(/ /g, '%20');
  const transformedLength = s.length;
  const transformedSpaces = (transformedLength - trimmedLength) / 2;

  if (trimmedSpaces !== 0 || transformedSpaces !== 0) {
    if (trimmedSpaces !== (transformedSpaces * 2)) {
      throw new Error ("Invalid input");
    }
  }

  return s;
}

module.exports = urlify;

And because I really don’t trust my ability to count spaces correctly, I repeat the above test with the following inputs:

'ApplesOranges '
'Apples Oranges   '
'Apples  Oranges  '

And all our tests pass.

Great!

Refactoring Code: going from the right code to good code

Since our tests pass, there’s not much else to do, right?

Well, since this is a mini-exploration of TDD, we’d better get onto refactoring. And that code does need some work.

The main thing that sticks out to me is all that checking of trimmedSpaces and transformedSpaces at the end. That really looks like a candidate for a new function to me.

Why put some perfectly good code into another function? Here’s a couple reasons:

  • If we give the new function a descriptive name, the purpose of that code becomes obvious. Right now it’s not that obvious.
  • We’ll be able to use the code elsewhere, and if we need to change the way spaces are validated, there’s only one, obvious place to change it.

Let’s get to it!

First, I’ll break out that code into a separate function, and call it as follows:

function urlSpacesValid(trimmedSpaces, transformedSpaces) {
  if (trimmedSpaces !== 0 || transformedSpaces !== 0) {
    if (trimmedSpaces !== (transformedSpaces * 2)) {
      return false;
    }
  }

  return true;
}

function urlify(s) {
  const originalLength = s.length;

  s = s.replace(/\s*$/, '');
  const trimmedLength = s.length;
  const trimmedSpaces = originalLength - s.length;

  s = s.replace(/ /g, '%20');
  const transformedLength = s.length;
  const transformedSpaces = (transformedLength - trimmedLength) / 2;

  if (! urlSpacesValid(trimmedSpaces, transformedSpaces)) {
      throw new Error ("Invalid input");
  }

  return s;
}

And I re-run the test:

npm t

> urlify@1.0.0 test ./urlify
> jasmine

Started
........


8 specs, 0 failures
Finished in 0.01 seconds

And all’s well.

A couple points about the code:

  • Since we’re using regular expressions to find those spaces, why don’t we use them to count the spaces? I chose to use string length instead just because it’s so much faster (computationally) than a regular expression. You could make an excellent ‘premature optimization’ argument against this approach 🙂
  • Why divide by 2 in one part of the code, and then multiply by 2 in another, when they essentially cancel each other out? Because I wanted to maintain a clear conceptual understanding that we’re validating our input by checking the number of spaces in them. I couldn’t think of a decent variable name for transformedLengh – trimmedLength, but once it’s divided by 2, it’s clearly the number of spaces that’ve been replaced.

Testing Times

You should know that it’s possible to run Jasmine from your browser. While that’s much prettier, it doesn’t interest me much, since I’d rather bundle my tests up with my build process.

If you’d like to find out more about that, head over to Jasmine’s github page for instructions – you’ll find it at the bottom of the Installation section.

There’s obviously a lot more to cover, and even our tiny 3-line urlify, incomplete as it is it still hasn’t been tested properly (can you see an absolutely glaring omission?)

But there’s only so much we can cover in one article, and we’ve actually covered 80% of what I use in day-to-day testing anyway – checking for expected values, and making sure errors are thrown when they should be.

If you’d like to know more, stay tuned for my next JavaScript testing article where I explore spies and asynchronous testing with Jasmine.

Notes:

  1. Yes, this is an insane way to prepare a string for use as or in an URL! See encodeURI() and encodeURIComponent() for this instead. There’s a good discussion here.

Mini-appendix: Installing npm

So first thing you need to know is that npm comes with node.js. So you get npm by installing node. If you’re not sure if you’ve got them installed or not, open a command line, and type

node -v

If you don’t have node installed this will cause an error. If you don’t get an error, stop reading! You’ve got node and npm installed 🙂

Otherwise, the easiest way to install Node.js – and by extension npm – is to use one of the installers from the Node downloads page. This will make life a lot easier.

If you’re using Linux, the Node.js website has excellent instructions on how you can use your distro’s package installer to get node and npm.

When you’ve finished installing, run node -v and npm -v to check everything’s working:

> node -v
v6.2.1
> npm -v
4.3.0
>

 

By


Leave a Reply

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