Javascript Specs with Jasmine - A Primer for Rubyists, Part 2

Chris Powers, Feb 01, 2011

In my last article I explained some of the benefits of using Jasmine for Javascript testing, especially for Rubyists familiar with RSpec. Now I would like to delve into Jasmine's syntax and show you how easy it is to pick up. Let's get cookin'...

Spec Suites

  # RSpec
  describe Cook do
    describe "baking a pie" do
      # ...
    end
  end

  // Jasmine
  describe("Cook", function() {
    describe("baking pie", function() {
      // ...
    });
  });

Just like RSpec, Jasmine test expectations are grouped together by nested describe blocks. More specifically, RSpec uses Ruby blocks for the nesting, while Jasmine relies on Javascript's anonymous functions. Each of these anonymous functions has its own scope, so variables declared within a describe will not spill outside it.

Before Each/ After Each

  # RSpec
  before(:each) do
    @pie = Pie.new
  end

  after(:each) do
    @pie.cleanup
  end

  // Jasmine
  var pie;
  beforeEach(function(){
    pie = new Pie();
  });

  afterEach(function() {
    pie.cleanup();
  });

As in RSpec, Jasmine has functions for setting up code that runs before and after each spec example. These can be nested inside of describe functions and will be applied to examples both in that describe function and all its nested blocks.

One important differentiation is that in RSpec the @pie variable can be declared in the before block, but is still available inside the scope of the after block (and all example specs). In Jasmine, however, you must declare all your variables outside of the before function, otherwise the variable would be exclusively scoped inside before function. For example:

  // Antipattern, won't work!
  beforeEach(function(){
    var pie = new Pie(); // scoped inside this function
  });

  afterEach(function() {
    pie.cleanup(); // error, 'pie' is undefined
  });

Before All / After All

  # RSpec
  before(:all) { setup_code }
  after(:all) { cleanup_code }
  describe "something" do
    # ... tests here
  end

  // Jasmine
  setupCode();
  describe("something", function() {
    // ... tests here
  });
  cleanupCode();

RSpec gives you the before(:all) and after(:all) methods to declare code that should be run once at the beginning or end of a describe block. Jasmine does not supply these methods because they are unneeded -- developers can simply run code before and after describe blocks to get the same effect.

Examples and Expectations

  # RSpec
  it "should be tasty" do
    pie = Pie.new
    pie.should be_tasty
  end

  // Jasmine
  it("should be tasty", function() {
    var pie = new Pie();
    expect(pie).toBeTasty();
  });

Jasmine shares RSpec's it syntax to build an example, again using anonymous functions rather than Ruby blocks. Within the it function, all expectations are powered by the expect method. RSpec has many methods that define expectations (should, should_not, should_receive, should_raise, etc.), but Jasmine has only one.

The expect method is passed an argument and then chained with a matcher method. Jasmine ships with several built-in matcher methods, and you can easily write your own custom matchers (like toBeTasty(), more on that later). A few of the built-in matchers are:

  • toBeTruthy()
  • toBeFalsy()
  • toMatch()
  • toInclude()
  • toBeUndefined()
  • And more...

WTF?! (What's the Falsy?!)

Most developers have the same reaction when they see matchers named toBeTruthy and toBeFalsy. "Um, huh? Why aren't they named toBeTrue and toBeFalse?"

There's a very good reason for this and it is apparent in the source code:

  jasmine.Matchers.prototype.toBeTruthy = function() {
    return !!this.actual;
  };

  jasmine.Matchers.prototype.toBeFalsy = function() {
    return !this.actual;
  };

These matchers evaluate this.actual rather than comparing it directly to true and false (ie this.actual === true). This can yield a few surprises if you're not familiar with how Javascript evaluates blank strings and zeros:

  expect("hello").toBeTruthy; // true
  expect("").toBeTruthy; // false
  expect(123).toBeTruthy; // true
  expect(0).toBeTruthy; // false

If you need to strictly test a value for true/false, you could do it like this:

  expect(value === true).toBeTruthy();

Negative Expectations

  # RSpec
  it "should not be burned" do
    pie = Pie.new
    pie.bake!
    pie.should_not be_burned
  end

  // Jasmine
  it("should not be burned", function(){
    var pie = new Pie();
    pie.bake();
    expect(pie).not.toBeBurned();
  });

In order to negate an expectation, Jasmine allows you to simply chain .not after using the expect method.

Custom Matchers

  # RSpec
  Spec::Matchers.define :be_burned do
    match do |actual|
      actual.color == "black"
    end
  end
  Spec::Matchers.define :be_color do
    match do |actual, color|
      actual.color == color
    end
  end

  // Jasmine
  beforeEach(function() {
    this.addMatchers({
      toBeBurned: function() {
        return this.actual.color == 'black';
      },
      toBeColor: function(color) {
        return this.actual.color == color;
      }
    });
  });

So how do we make nifty matchers like .toBeBurned using Jasmine? Pretty similarly to RSpec (no surprises there)! In a beforeEach function, outside of any describe statements, you can use the this.addMatchers method, passing an object with matcher name keys and function values. Inside each matcher function, this.actual magically represents the object with the expectation, and any arguments passed to the matcher are available as parameters in the matcher function.

Stubbing Values

  # RSpec
  it "should cook until 160 degrees" do
    pie = Pie.new
    pie.stub!(:hit_web_service)
    pie.stub!(:temperature).and_return(160)
    pie.done_baking?.should be_true
  end

  // Jasmine
  it("should cook until 160 degrees", function() {
    var pie = new Pie();
    spyOn(pie, 'hitWebService');
    spyOn(pie, 'temperature').andReturn(160);
    expect(pie.doneBaking()).toBeTruthy();
  });

Sometimes in tests you need to stub out values on an object. In the Ruby example, you might want to stub out the hit_web_service method to speed up your tests, or because the service isn't available in your test environment. More commonly, we want to stub out methods like temperature so that we can return a particular value and see what happens. In this example, we can ensure that the pie object is done baking if given the specific temperature of 160 degrees.

While Jasmine offers the same functionality, it supplies it in a slightly different package. Rather than stubs, Jasmine implements spies. A spy object is created using the spyOn function, which accepts an object and the name of a method on that object. Jasmine removes the function from the object (remember, functions are objects in Javascript) and replaces it with a spy object.

By default, the spy will silently absorb any calls it receives, which effectively stubs out that method. You can also chain the andReturn method to the spy in order to instruct that spy to respond to calls with the given value. While the under-the-hood implementations are different, the net effect of the Ruby and Jasmine examples above are equal.

Message Expectations

  # RSpec
  it "should bake a pie for dinner" do
    cook = Cook.new
    cook.should_receive(:bake_a_pie!)
    cook.make_dinner!
  end

  // Jasmine
  it("should bake a pie", function() {
    var cook = new Cook();
    spyOn(cook, 'bakePie');
    cook.makeDinner();
    expect(cook.bakePie).toHaveBeenCalled();
  });

Using RSpec, the should_receive method does two things. First, it stubs out the given method just like stub!, allowing the chaining of methods like and_return to mock behavior. Second, it places an expectation that the method should be called in the future. Subsequently, all should_receive calls must be made before calling the functionality that is being tested.

Jasmine matches this ability, but again does so using spies. Since the stubbed method is replaced by the spy object, the spy can be referenced using the method name (ie cook.bakePie returns the spy object). Expectations can be placed on this spy as easily as any other kind of object. In this example, we place an expectation on the spy that it should have been called using the built-in matcher toHaveBeenCalled. Note that while RSpec forces you to set expectations of what will happen, Jasmine allows you to set expectations of what should have already happened.

Message Expectations with Arguments

  # RSpec
  it "should bake a TASTY pie" do
    cook = Cook.new
    cook.should_receive(:bake_a_pie!).with('TASTY')
    cook.make_dinner!
  end

  // Jasmine
  it("should bake a TASTY pie", function() {
    var cook = new Cook();
    spyOn(cook, 'bakePie');
    cook.makeDinner();
    expect(cook.bakePie).toHaveBeenCalledWith('TASTY');
  });

If you need to specify in Jasmine that a method should have been called with certain arguments, you can do this by using the toHaveBeenCalledWith matcher rather than toHaveBeenCalled.

Calling Through to the Original Method

  # RSpec
  # Sad panda, RSpec doesn't do this...

  // Jasmine
  it("should bake a TASTY pie", function() {
    var cook = new Cook();
    spyOn(cook, 'bakePie').andCallThrough();
    cook.makeDinner();
    expect(cook.bakePie).toHaveBeenCalledWith('TASTY');
  });

In RSpec, stubbing out a method means saying goodbye to it -- you can mock the method's behavior and return something specific, but you can't observe the calls the method receives while still maintaining its original functionality.

Not so with Jasmine. If you need to transparently spy on a method without "stubbing it out", you can do so by calling andCallThrough on your spy object. The method is still replaced by the spy object, but its reference is retained and the spy will pass all calls through to the original method. Expectations on the spy work the same way as before.

Expecting a Number of Messages

  # RSpec
  it "should check the pie twice" do
    cook = Cook.new
    cook.should_receive(:check_the_pie).twice
    cook.bake_a_pie!
  end

  // Jasmine
  it("should check the pie twice", function() {
    var cook = new Cook();
    spyOn(cook, 'checkThePie');
    cook.bakePie();
    expect(cook.checkThePie.callCount).toEqual(2);
  });

Thanks to the wonders of Ruby, RSpec gives you some nice chaining methods like twice to place an expectation on the number of times an object should receive a given method call. Jasmine gives you the same functionality in a decidedly less sexy syntax. All spies have a callCount attribute that is incremented every time the spy is called. This allows you to check that the callCount equals 2 at the end of the spec.

Error Expectations

  # RSpec
  it "should refuse to bake a pie" do
    cook = Cook.new("cranky")
    lambda do
      cook.bake_a_pie!
    end.should raise_error(MakeYourOwnDamnPie)
  end

  // Jasmine
  it("should refuse to bake a pie", function() {
    var cook = new Cook('cranky');
    var msg = "Make Your Own Damn Pie!";
    expect(cook.bakePie).toThrow(msg);
  });

This is an instance where I think Jasmine really outshines RSpec. If you are expecting an error to be thrown in RSpec, you need to wrap the offending code in a lambda block and place the expectation on the block itself. It works, but it feels awkward to me. In Jasmine, you can use the toThrow matcher directly on the spy expectation. This will automatically run the original spy method (cook.bakePie()) and expect it to throw the given msg message.

Simulating Errors

  # RSpec
  it "should catch errs and retry with sudo" do
    cook = Cook.new
    cook.stub!(:bake_a_pie!).and_raise(MakeYourOwnDamnPie)
    cook.should_receive(:sudo_bake_a_pie!)
    cook.make_dinner!
  end

  // Jasmine
  it("should retry with sudo", function() {
    var cook = new Cook();
    spyOn(cook, 'bakePie').andThrow("Make Your Own Damn Pie!");
    spyOn(cook, 'sudoBakePie');
    expect(cook.sudoBakePie).toHaveBeenCalled();
  });

Finally, sometimes you want to simulate an error being throw so that you can observe how your application catches it. RSpec allows for this by stubbing a method and then chaining it with and_raise. Jasmine follows suit closely, allowing you to chain andThrow to a spy. In this case, the expectation is that when the cook is told to 'bake a pie' and throws "Make Your Own Damn Pie!", the application catches the error and then calls sudoBakePie (works every time).

Phew, well if you've made it this far you should be all set to test drive your Javascript using Jasmine. Now if you'll excuse me, I'm going to go get some pie, this article has made me hungry.

Jasmine Resources

Rss-icon Rss-icon-over
Archive

Archive