Testing JavaScript Using the Jasmine Framework

Intro to Testing.

Testing is useful for a number of reasons. First, these tests can evaluate a program’s correctness after a change. These tests can also function as good examples for other developers. If a developer is trying to figure out how to use some undocumented part of your code, a well-written test can help him see how that piece works.

A relatively new software development technique is called Test-Driven Development, or TDD. With behavior-driven development, or BDD, you write specifications that are small and easy to read.

TDD in its simplest form is just this:

  1. Write your tests
  2. Watch them fail
  3. Make them pass
  4. Refactor
  5. Repeat

BDD is a little more complex: it’s more of a team thing. Here are a few of the practices of BDD:

  • Establishing the goals of different stakeholders required for a vision to be implemented
  • Involving stakeholders in the implementation process through outside-in software development
  • Using examples to describe the behavior of the application, or of units of code
  • Automating those examples to provide quick feedback and regression testing

What Is Jasmine?

Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.

Why Jasmine?

Jasmine is an automated testing framework for JavaScript.

Testing in a nutshell: basically, your program will have a bunch of functions and classes. You want to make sure that, no matter what you throw at them, they’ll perform how you want them to. For example, this function should always return a string that says "hello" in it. Testing ensures that everything goes down exactly how you planned. It’s like you’re God…but it’s probably a little more boring because it’s code.

If you’ve used languages other than JavaScript before, you might use JavaScript and get angry. If you’ve used other testing frameworks, I am sorry if the same frustration happens while you use Jasmine — she changes the names of some stuff and she also makes fun of you. She called me a “turd-faced coward”.

Jasmine is a testing framework for JavaScript. Let’s learn all of her secrets.

Getting Set Up with Jasmine.

Jasmine can be used by itself; or you can integrate it with a Rails project. We’ll do the former. While Jasmine can run outside the browser (think Node, among other places), we can get a really nice little template with the download.

Start by downloading the latest standalone release of Jasmine. Unzip it.

You’ll find the actual Jasmine framework files in the lib folder. There’s actually some sample code wired up in this project template. The “actual” JavaScript ( the code we want to test) can be found in the src subdirectory; we’ll be putting ours there shortly. The testing code—the specs—go in the spec folder.

When you open SpecRunner.html in a web browser, you’ll see something like this figure.

 

This file has run some example tests on some example code. It’s testing a Player and a Song. Whenever you want to run the tests, you simply need to load/reload this page.

In the src directory, you’ll see two things to be tested: a  Player  and a  Song. The spec directory has tests for the Player. Taking a look inside the  specdirectory might help you understand Jasmine’s syntax (though there’s also this fine book to help with that).
You probably don’t want to test this example code, so you should empty out the spec and src directories. When you change the filenames, you’ll have to edit  SpecRunner.html to point to the right files (there are comments that indicate what you should change). We’ll go through how to do that next.

Learning the Syntax.

If you’re at all familiar with Rspec, the de facto BDD framework, you’ll see that Jasmine takes a lot of cues from Rspec. Jasmine tests are primarily two parts: describe blocks and it blocks. Let’s see how this works.

We’ll look at some closer-to-real-life tests in a few, but for now, we’ll keep it simple:

// JavaScript addition operator
describe('JavaScript addition operator', function () {
    it('adds two numbers together', function () {
        expect(1 + 2).toEqual(3);
    });
});

Suites: describe Your Tests

A test suite begins with a call to the global Jasmine function describe with two parameters: a string and a function. The string is a name or title for a spec suite – usually what is being tested. The function is a block of code that implements the suite.

Specs

Specs are defined by calling the global Jasmine function it, which, like describe takes a string and a function. The string is the title of the spec and the function is the spec, or test. A spec contains one or more expectations that test the state of the code. An expectation in Jasmine is an assertion that is either true or false. A spec with all true expectations is a passing spec. A spec with one or more false expectations is a failing spec.

It’s Just Functions

Since describe and it blocks are functions, they can contain any executable code necessary to implement the test. JavaScript scoping rules apply, so variables declared in a describe are available to any it block inside the suite.

Expectations

Expectations are built with the function expect which takes a value, called the actual. It is chained with a Matcher function, which takes the expected value.

Matchers

Each matcher implements a boolean comparison between the actual value and the expected value. It is responsible for reporting to Jasmine if the expectation is true or false. Jasmine will then pass or fail the spec.

Any matcher can evaluate to a negative assertion by chaining the call to expect with a not before calling the matcher.

// The 'toEqual' matcher
describe("The 'toEqual' matcher", function() {

    it("should work for objects", function() {
        var foo = {
            a: 12,
            b: 34
        };
        var bar = {
            a: 12,
            b: 34
        };
        expect(foo).toEqual(bar);
    });

    it("The 'toMatch' matcher is for regular expressions", function() {
        var message = "foo bar baz";

        expect(message).toMatch(/bar/);
        expect(message).toMatch("bar");
        expect(message).not.toMatch(/quux/);
    });

    it("The 'toBeDefined' matcher compares against `undefined`", function() {
        var a = {
          foo: "foo"
        };

        expect(a.foo).toBeDefined();
        expect(a.bar).not.toBeDefined();
    });

    it("The `toBeUndefined` matcher compares against `undefined`", function() {
        var a = {
          foo: "foo"
        };

        expect(a.foo).not.toBeUndefined();
        expect(a.bar).toBeUndefined();
    });

    it("The 'toBeNull' matcher compares against null", function() {
        var a = null;
        var foo = "foo";

        expect(null).toBeNull();
        expect(a).toBeNull();
        expect(foo).not.toBeNull();
    });

    it("The 'toBeTruthy' matcher is for boolean casting testing", function() {
        var a, foo = "foo";

        expect(foo).toBeTruthy();
        expect(a).not.toBeTruthy();
    });

    it("The 'toBeFalsy' matcher is for boolean casting testing", function() {
        var a, foo = "foo";

        expect(a).toBeFalsy();
        expect(foo).not.toBeFalsy();
    });

    it("The 'toContain' matcher is for finding an item in an Array", function() {
        var a = ["foo", "bar", "baz"];

        expect(a).toContain("bar");
        expect(a).not.toContain("quux");
    });

    it("The 'toBeLessThan' matcher is for mathematical comparisons", function() {
        var pi = 3.1415926,
          e = 2.78;

        expect(e).toBeLessThan(pi);
        expect(pi).not.toBeLessThan(e);
    });

    it("The 'toBeGreaterThan' is for mathematical comparisons", function() {
        var pi = 3.1415926,
          e = 2.78;

        expect(pi).toBeGreaterThan(e);
        expect(e).not.toBeGreaterThan(pi);
    });

    it("The 'toBeCloseTo' matcher is for precision math comparison", function() {
        var pi = 3.1415926,
          e = 2.78;

        expect(pi).not.toBeCloseTo(e, 2);
        expect(pi).toBeCloseTo(e, 0);
    });

    it("The 'toThrow' matcher is for testing if a function throws an exception", function() {
        var foo = function() {
            return 1 + 2;
        };
        var bar = function() {
            return a + 1;
        };

        expect(foo).not.toThrow();
        expect(bar).toThrow();
    });
});

In the above example, the toBeGreaterThan() method is a matcher. It represents a comparison such as “>” using plain English. There are many other matchers available, including:

  • toBe: represents the exact equality (===) operator.
  • toEqual: represents the regular equality (==) operator.
  • toMatch: calls the RegExp match() method behind the scenes to compare string data.
  • toBeDefined: opposite of the JS “undefined” constant.
  • toBeUndefined: tests the actual against “undefined”.
  • toBeNull: tests the actual against a null value – useful for certain functions that may return null, like those of regular expressions (same as toBe(null))
  • toBeTruthy: simulates JavaScript boolean casting.
  • toBeFalsy: like toBeTruthy, but tests against anything that evaluates to false, such as empty strings, zero, undefined, etc…
  • toContain: performs a search on an array for the actual value.
  • toBeLessThan/toBeGreaterThan: for numerical comparisons.
  • toBeCloseTo: for floating point comparisons.
  • toThrow: for catching expected exceptions.

Covering Before and After

Often—when testing a code base—you’ll want to perform a few lines of set-up code for every test in a series. It would be painful and verbose to have to copy that for every it call, so Jasmine has a handy little feature that allows us to designate code to run before or after each test. Let’s see how this works:

// MyObject beforeEach
describe("MyObject", function () {
    var obj = new MyObject();

    beforeEach(function () {
        obj.setState("clean");
    });

    it("changes state", function () {
        obj.setState("dirty");
        expect(obj.getState()).toEqual("dirty");
    })
    it("adds states", function () {
        obj.addState("packaged");
        expect(obj.getState()).toEqual(["clean", "packaged"]);
    })
});

In this contrived example, you can see how, before each test is run, the state of obj is set to “clean”. If we didn’t do this, the changed made to an object in a previous test persist to the next test by default. Of course, we could also do something similar with the AfterEach function:

// MyObject afterEach
describe("MyObject", function () {
    var obj = new MyObject("clean"); // sets initial state

    afterEach(function () {
        obj.setState("clean");
    });

    it("changes state", function () {
        obj.setState("dirty");
        expect(obj.getState()).toEqual("dirty");
    })
    it("adds states", function () {
        obj.addState("packaged");
        expect(obj.getState()).toEqual(["clean", "packaged"]);
    })
});

MyObject.js

// MyObject
var MyObject = function (state) {
    this._state = state;
}
MyObject.prototype.setState = function (state) {
    this._state = state;
};
MyObject.prototype.addState = function (state) {
    if (typeof this._state === "string") {
        this._state = [this._state, state];
    } 
    else {
        this._state.push(state);
    }
};
MyObject.prototype.getState = function () {
    return this._state;
}

Reference

[1] Jasmine – behavior-driven development framework.

[2] JavaScript Testing with Jasmine – Evan Hahn

Add a Comment

Scroll Up