Larry Price

And The Endless Cup Of Coffee

Jasmine - a Whole New World of Javascript Testing

| Comments

Jasmine: a headless Javascript testing library written entirely in Javascript. With similarities to rspec, I’ve quickly grown attached to this framework and have been looking for opportunties to discuss it. Version 2.0 was recently released, so I’ll be focusing on the standalone 2.0 concepts. To get started, download and uncompress the standalone distribution.

The uncompressed directory structure will have three subdirectories: spec, src, and lib. lib contains all the Jasmine source code. src contains some sample Javascript class that is tested by test files contained in spec. Outside of the subdirectories is the special file SpecRunner.html. This file is how we will run our tests.

Let’s start a new pizza place.

We’ll need Pizza. A Pizza will need several things: size, style, toppings, and price. We’ll have a few styles available, but also allow our guests to request additional toppings. We’ll also set the price based on the size and number of toppings. Create the files src/pizza.js and spec/PizzaSpec.js and add them to SpecRunner.html.

We’ll start by being able to get the styles from Pizza.

spec/PizzaSpec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
describe("Pizza", function() {
  var pizza;

  beforeEach(function() {
    pizza = new Pizza();
  });

  it("should give a choice of styles", function() {
    expect(pizza.getStyles()).toContain("meat lovers");
    expect(pizza.getStyles()).toContain("veg head");
    expect(pizza.getStyles()).toContain("supreme");
  });
});

The syntax is just lovely: We use describe to set visual context, beforeEach to perform a task before each spec, and it to encapsulate a test. The results of running SpecRunner.html in my browser:

spec/PizzaSpec.js
1
2
Pizza should give a choice of styles
  TypeError: pizza.getStyles is not a function in file:///home/lrp/docs/jasmine/spec/PizzaSpec.js (line 9)

Fixing it:

src/pizza.js
1
2
3
4
5
function Pizza() {
  this.getStyles = function() {
    return ["meat lovers", "veg head", "supreme"];
  }
}

And the results:

src/pizza.js
1
2
Pizza
    should give a choice of styles

Let’s set the toppings:

spec/PizzaSpec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
describe("Pizza", function() {
  // ...

  describe("toppings", function() {
    it("should have no toppings when no style and no extras given", function() {
      pizza.initialize();
      expect(pizza.getToppings().length).toBe(0);
    });

    it("should have only extras when no style and extras given", function() {
      var extras = ["pineapple", "edamame", "cheeseburger"]
      pizza.initialize(null, null, extras);

      expect(pizza.getToppings().length).toBe(extras.length);
      for (var i = 0; i < extras.length; i++) {
        expect(pizza.getToppings()).toContain(extras[i]);
      }
    });

    it("should have special toppings when given style and extras", function() {
      var extras = ["pineapple", "edamame", "cheeseburger"];
      pizza.initialize(null, "veg head", extras);

      expect(pizza.getToppings().length).toBe(7);
    });

    it("should have special toppings when given style", function() {
      var extras = ["pineapple", "edamame", "cheeseburger"];
      pizza.initialize(null, "veg head");

      expect(pizza.getToppings().length).toBe(4);
    });
  });
});

For these tests, I nested a describe block to give better context to what I’m testing. Fixing the tests:

src/pizza.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function Pizza() {
  // ...

  var size, toppings;

  function findToppings(style, extras) {
    toppings = extras ? extras : [];

    switch (style) {
      case ("meat lovers"):
        toppings.push("ham", "pepperoni", "bacon", "sausage");
        break;
      case ("veg head"):
        toppings.push("onion", "tomato", "pepper", "olive");
        break;
      case ("supreme"):
        toppings.push("pepperoni", "onion", "sausage", "olive");
        break;
    }
  }

  this.getToppings = function() {
    return toppings;
  };

  this.initialize = function(pizzaSize, style, extras) {
    size = pizzaSize;
    findToppings(style, extras);
  };
}

And finally, I’ll deal with the cost. I’ll come out of scope of the nested describe and nest another describe.

spec/PizzaSpec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe("Pizza", function() {
  // ...

  describe("cost", function() {
    it("is detemined by size and number of toppings", function() {
      pizza.initialize(10, "supreme");
      expect(pizza.getToppings().length).toBe(4);
      expect(pizza.getCost()).toBe(7.00);
    });

    it("is detemined by size and number of toppings including extras", function() {
      pizza.initialize(18, "meat lovers", ["gyros", "panchetta"]);
      expect(pizza.getToppings().length).toBe(6);
      expect(pizza.getCost()).toBe(12.00);
    });
  });
});

To fix this test, I’ll use my handy-dandy pizza-cost forumla:

src/pizza.js
1
2
3
4
5
6
7
8
9
function Pizza() {
 // ...

  this.getCost = function() {
    return size/2 + toppings.length * .5;
  }

  // ...
}

This is great and all, but a bit simple. What if we wanted to make an ajax call? Fortunately, I can fit that into this example using Online Pizza, the pizza API. Unfortuantely, the API is kind of garbage, but that doesn’t make this example any more meaningless. You can download jasmine-ajax on Github, and stick it in your spec/ directory and add it to SpecRunner.html. At this point I need to include jquery as well.

In order to intercept ajax calls, I’ll install the ajax mocker in the beforeEach and uninstall it in an afterEach. Then I write my test, which verifies that the ajax call occurred and returns a response.

spec/PizzaSpec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
beforeEach(function() {
  jasmine.Ajax.install();

  pizza = new Pizza();
});

afterEach(function() {
  jasmine.Ajax.uninstall();
});

describe("sendOrder", function() {
  it("returns false for bad pizza", function() {
    pizza.sendOrder();

    expect(jasmine.Ajax.requests.mostRecent().url).toBe("http://onlinepizza.se/api/rest?order.send");

    jasmine.Ajax.requests.mostRecent().response({
      status: "500",
      contentType: "text/plain",
      responseText: "Invalid pizza"
    });

    expect(pizza.orderSent()).toBe(false);
  });

  it("returns true for good pizza", function() {
    pizza.sendOrder();

    expect(jasmine.Ajax.requests.mostRecent().url).toBe("http://onlinepizza.se/api/rest?order.send");

    jasmine.Ajax.requests.mostRecent().response({
      status: "200",
      contentType: "text/plain",
      responseText: "OK"
    });

    expect(pizza.orderSent()).toBe(true);
  });
});

To get this to work, I add some logic to the Pizza class to set some state based on what the ajax call returns.

src/pizza.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var orderSuccess;

this.sendOrder = function() {
  orderSuccess = null;

  $.ajax({
    type: "POST",
    url: "http://onlinepizza.se/api/rest?order.send",
    success: function() {
      orderSuccess = true;
    },
    error: function() {
      orderSuccess = false;
    }
  });
}

this.orderSent = function() {
  return orderSuccess;
}

Ajax calls tested. By installing Jasmine’s ajax mock, all of the ajax calls were intercepted and were not sent to the server at Online Pizza. Any ajax calls that may have been fired by the Pizza class but were not addressed in the spec are ignored. The final test results look something like this:

src/pizza.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Pizza
    sendOrder
        returns false for bad pizza
        returns true for good pizza
    styles
        should give a choice of styles
    toppings
        should have no toppings when no style and no extras given
        should have only extras when no style and extras given
        should have special toppings when given style and extras
        should have special toppings when given style
    cost
        is detemined by size and number of toppings
        is detemined by size and number of toppings including extras

Full sample code available on Github. There’s a lot of other interesting things Jasmine can do that I’m still learning about. If applicable, I’ll try to create a blog post for advanced Jasmine usage in the future.

Comments