About Me

  • Senior UI Developer at Hobsons, where we build cool stuff to help teachers, students, and school counselors
  • Author of an upcoming book on Jasmine
  • Publisher of A Drip of JavaScript, a weekly JavaScript newsletter

You're here to learn something

So you probably have some questions

Like...

  • What is unit testing?
  • What is Test Driven Development?
  • What is Jasmine?
  • How can these things help me?
  • How do I use Jasmine?

So I have some questions for you

Raise your hands for yes.

  • How many of you are primarily JS developers?
  • How many of you know what unit testing is?
  • How many of you write unit tests for your JS?
  • How many of you know what Test Driven Development is?
  • How many of you do TDD for your JS?
  • How many of you have heard of Jasmine before this talk?
  • How many of you use Jasmine?
Description

Unit Testing is…

  • the practice of writing code
  • which tests the correctness of
  • small(ish) units of your code.
describe("Addition function", function() {
  it("should add numbers", function() {
    expect(add(1, 1)).toBe(2);
  });
});

Test Driven Development (TDD) is…

The practice of writing your tests first, so that they can guide your implementation.

The steps of doing TDD are:

  1. Write your test first, and see it fail. (Red)
  2. Write your unit of code and see the test pass. (Green)
  3. Make your code better, using the tests to keep you safe. (Refactor)

Jasmine is…

A spunky Disney princess?

Just kidding. :-)

Jasmine is…

  • A JavaScript testing library
  • With an easy to use syntax
  • That can be used in the browser
  • Or on the command line

Tests vs. Specs

What's the difference?

They're the same thing.

Jasmine uses the term "specs" to emphasize that they should be written before you start coding.

What a Jasmine Suite Looks Like

  • At least one describe block (they can be nested)
  • At least one it block which contains a spec
  • At least one expectation, which consists of:
    • expect which is passed an expression (called the "actual" value)
    • A matcher with the expected value (toBe and toBeGreaterThan)
describe("Calculator", function() {
  describe("Addition function", function() {
    it("should add numbers", function() {
      expect(add(1, 1)).toBe(2);
      expect(add(2, 2)).toBeGreaterThan(3);
    });
  });
});

The Spec Runner

Is just an ordinary HTML file that you load in your browser.

Include the Jasmine library, your sources, your specs, and you have a test environment.

<link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>

<!-- include source files here... -->
<script type="text/javascript" src="src/Player.js"></script>
<script type="text/javascript" src="src/Song.js"></script>

<!-- include spec files here... -->
<script type="text/javascript" src="spec/SpecHelper.js"></script>
<script type="text/javascript" src="spec/PlayerSpec.js"></script>

Using Jasmine

The Test-Driven Way

The Plan

We're going to write a mini string library which has the following functions.

  1. A "first word" function which returns the first word of a string.
  2. An "nth word" function which returns any word n of a string.

First we describe it

  • We're testing the stringUtil library.
  • Specifically we're testing the firstWord function.
describe("stringUtil", function() {
  describe("firstWord", function() {
    // Specs go here.
  });
});

Then we spec it

describe("stringUtil", function() {
  describe("firstWord", function() {
    it("should return the first word of a string", function () {
      expect(stringUtil.firstWord("one two")).toBe("one");
    });
  });
});

Then we run the specs

Confused?

Remember the TDD process.

  • Red -> Green -> Refactor
  • If you don't get red after writing your specs first, then there is something wrong with your specs.
  • Basically, that first red means you're doing the right thing.

Back to that failure

stringUtil isn't defined.

So let's define it

var stringUtil = {};

Now we get a different error

stringUtil doesn't have a firstWord method

Let's fix that

var stringUtil = {
  firstWord: function() {
    // Not doing anything yet.
  }
};

Our function isn't returning anything

Now for some actual logic

var stringUtil = {
  firstWord: function(text) {
    var textWords = text.split(" ");
    return textWords[0];
  }
};

We got to green!

But that code wasn't the greatest

Time to refactor

var stringUtil = {
  firstWord: function(text) {
    return text.split(" ")[0];
  }
};

Green again!

Now lets write the specs for nthWord

describe("stringUtil", function() {
  describe("firstWord", function() {
    it("should return the first word of a string", function () {
      expect(stringUtil.firstWord("one two")).toBe("one");
    });
  });

  describe("nthWord", function() {
    it("should return the nth word of a string", function () {
      expect(stringUtil.nthWord("one two", 1)).toBe("one");
      expect(stringUtil.nthWord("one two", 2)).toBe("two");
    });
  });
});

We get our first failing spec

stringUtil doesn't have a nthWord method

Let's write nthWord

var stringUtil = {
  firstWord: function(text) {
    return text.split(" ")[0];
  },

  nthWord: function(text, i) {
    return text.split(" ")[i - 1];
  }
};

Green on the first try!

What about refactoring?

  • In this case our nthWord method is so simple that there isn't really anything to refactor in the method itself.
  • But you might have noticed that we have some repetition going on between our two methods.
    • We could refactor that out now.
    • Or we could introduce another test that will help us catch this repetition.
var stringUtil = {
  firstWord: function(text) {
    return text.split(" ")[0];
  },

  nthWord: function(text, i) {
    return text.split(" ")[i - 1];
  }
};

Testing implementation vs. interface

  • Normally when testing, you should only be testing the interface, not the implementation.
  • But in certain cases, you may want to ensure that a method is implemented in a certain way.
    • If you are performance sensitive and need to delegate to a heavily optimized method.
    • If the logic is complicated to get right and you need to be absolutely sure that it is delegated to a dedicated method.
    • If you are delegating to an external library and need to ensure that the correct parameters are sent.

Let's use our imagination!

Here's our updated spec for firstWord

describe("firstWord", function() {
  it("should return the first word of a string", function () {
    expect(stringUtil.firstWord("one two")).toBe("one");
  });

  it("should delegate its logic to nthWord", function () {
    spyOn(stringUtil, "nthWord");
    stringUtil.firstWord("one two");

    expect(stringUtil.nthWord).toHaveBeenCalled();
  });
});

So, you're a spy?

Jasmine spies are like double-agents. They replace an ordinary function and report back what they're doing.

They can:

  • Report that they were called.
  • Report how many times they were called.
  • Report what parameters they were called with.
  • Invoke the original function which they replaced.
    (If needed to preserve functionality in the test).

Our spec fails

nthWord isn't being called

We rework firstWord to delegate to nthWord

var stringUtil = {
  firstWord: function(text) {
    return this.nthWord(text, 1);
  },

  nthWord: function(text, i) {
    return text.split(" ")[i - 1];
  }
};

And everything is green

Do we need to refactor?

Looks clean to me!

var stringUtil = {
  firstWord: function(text) {
    return this.nthWord(text, 1);
  },

  nthWord: function(text, i) {
    return text.split(" ")[i - 1];
  }
};

So why go through all that?

  • Leads to better code design by encouraging smaller units of code which you can combine to achieve your larger goals.
  • Having tests/specs lets you refactor with confidence.
  • Writing the specs first means that they are going to be more complete, so you can have more confidence in them.
  • Since you saw the specs fail, you know that they can fail. Which is a great thing.
  • The more confidence you have in your tests/specs, the faster you can improve your existing code.
  • Obviously, the bigger and more important a system you're building, the more helpful these techniques will be.

Enough TDD stuff

Let's talk more about Jasmine

Lots of matchers available

Matcher Description
toBe Compares actual and expected with ===
toEqual Compares simple object literals that === cannot
toMatch Compares actual value against a regular expression
toBeDefined Checks to see if the actual value is defined
toBeUndefined Checks to see if the actual value is undefined
toBeNull Checks to see if the actual value is null
toBeTruthy Checks to see if the actual value coerces to true

More matchers

Matcher Description
toBeFalsy Checks to see if the actual value coerces to false
toContain Checks to see if an array contains the expected value
toBeLessThan Self-explanatory
toBeGreaterThan Self-explanatory
toBeCloseTo For precision math comparisons on floating point numbers
toThrow Checks to see if an error is thrown
toHaveBeenCalled Checks to see if the spy was called
toHaveBeenCalledWith Checks to see if the spy was called with the expected parameters

And those are just the built-ins

There are Jasmine plugins as well

  • Jasmine jQuery

    Provides matchers for jQuery objects, and an easy way to build HTML fixtures to test with.

  • Underscore Matchers for Jasmine

    Provides matchers based on the methods in Underscore.js. Particularly useful for Backbone projects.

  • AngularJS Matchers

    Provides matchers for working with the Angular JS framework.

You can also write your own matchers

This example checks the last element in an array to see if it matches the expected value.

this.addMatchers({
  toEndWith: function(expected) {
    return this.actual[this.actual.length - 1] === expected;
  }
});

More handy features of Jasmine

  • Spies:
    Are extremely powerful. Can be used to fake objects and functions in more ways than we have time to cover.
  • Async testing:
    Is Jasmine's weak point. However, the next major version of Jasmine will have a revamped API for async testing.
  • jasmine.any
    A helper utility that lets you match against a constructor or "class".

    expect(red).toBe(jasmine.any(Color));
    

References

  • The Docs

    Are good, though they can be a little hard to follow if you're new to Jasmine.
    The cool thing about them is that they are a self-executing Jasmine spec describing Jasmine.

  • Try Jasmine

    An interactive in-browser console for trying out Jasmine. It's a great way to learn.

Powerups

Tools to Improve Your Jasmine Testing

Writing Specs in CoffeeScript

JavaScript Version:

describe("stringUtil", function() {
  describe("firstWord", function() {
    it("should return the first word of a string", function () {
      expect(stringUtil.firstWord("one two")).toBe("one");
    });

    it("should delegate its logic to nthWord", function () {
      spyOn(stringUtil, "nthWord");
      stringUtil.firstWord("one two");

      expect(stringUtil.nthWord).toHaveBeenCalled();
    });
  });

  describe("nthWord", function() {
    it("should return the nth word of a string", function () {
      expect(stringUtil.nthWord("one two", 1)).toBe("one");
      expect(stringUtil.nthWord("one two", 2)).toBe("two");
    });
  });
});

Writing Specs in CoffeeScript

CoffeeScript Version:

describe "stringUtil", ->
  describe "firstWord", ->
    it "should return the first word of a string", ->
      expect(stringUtil.firstWord("one two")).toBe "one"

    it "should delegate its logic to nthWord", ->
      spyOn stringUtil, "nthWord"
      stringUtil.firstWord "one two"
      expect(stringUtil.nthWord).toHaveBeenCalled()

  describe "nthWord", ->
    it "should return the nth word of a string", ->
      expect(stringUtil.nthWord("one two", 1)).toBe "one"
      expect(stringUtil.nthWord("one two", 2)).toBe "two"

CoffeeScript specs end up being a lot cleaner and easier to read.

Jasmine in the Terminal

What it looks like:

Jasmine in the Terminal

Benefits

  • Specs can run as part of continuous integration test suite, just like your back-end code.
  • You don't have to run a browser to see what's going on.
  • You can automatically kick off your specs every time you save.
    Instant feedback makes it much easier to keep moving forward.

Jasmine in the Terminal

How it works

It depends on:

  • Node
  • Grunt (JavaScript based build tool)
  • The Watch plugin for Grunt
  • Jasmine plugin for Grunt
    (Uses the PhantomJS headless web browser)

Jasmine in the Terminal

Example configuration

module.exports = function(grunt) {
  grunt.loadNpmTasks("grunt-contrib-jasmine");
  grunt.loadNpmTasks("grunt-contrib-watch");
  
  grunt.initConfig({
    jasmine: {
      test: {
        src: ["lib/jquery.min.js", "src/mySource.js"],
        options: {
          specs: "spec/mySpec.js"
        }
      }
    },
    
    watch: {
      files: ["src/mySource.js", "spec/mySpec.coffee"],
      tasks: ["default"]
    }
  });
  
  grunt.registerTask("default", ["jasmine:test"]);
};

Closing Notes

Jasmine Testing: A Cloak & Dagger Guide

It's a book! With a discount!

  • The book is still in-progress, but you can buy it now and you'll get each chapter as it is written.
  • Regular price is $18, but you're getting a 75% discount!
  • $4.50 is the same price as a sugary Starbucks drink. :-)
  • The coupon code is novawebdev. It will be good for the next month.
  • You can find the book at: jasminetesting.com.

A Drip of JavaScript

One quick JavaScript tip, delivered to your inbox weekly.

You can find it at: adripofjavascript.com

These Slides

They're on GitHub

You have questions?

I have answers. (I hope!)