Ember.js testing with Jasmine

By Chris Sprehe
3 months ago

Ember.js testing with Jasmine

Introduction

Before diving right into the Jasmine Ember.Test.Adapter I wrote, let's take a quick look at the differences with Async control using Qunit vs. Jasmine - see Asynchronous Support, along with Ember's primary async control method used in its integration test helper suite.

Ember.js integration helpers - wait function

The gist of this method is it pauses the test runner suite while Ember internally finishes running any asynchronous goodness. It does this using a Javascript interval that polls every 10ms -- as long as the async wheels are still turning, the interval keeps polling. To look at the full implementation of this method, pry open Ember's helper.js file and look at function wait.

To pause the test suite, Ember relies on the implementation of the test adapter's asyncStart and asyncEnd functions. Your asyncStart function, which runs at the top of the Ember helper wait function, should pause the test runner suite:

// If this is the first async promise, kick off the async test
if (++countAsync === 1) {
  Test.adapter.asyncStart();
}

Your asyncEnd function, which runs at the bottom of the polling interval once any async logic finishes, should hit 'play' on the test runner suite:

// If this is the last async promise, end the async test
if (--countAsync === 0) {
  Test.adapter.asyncEnd();
}

QUnit Async Control

In understanding the async implementation with the Ember.js library using QUnit, we really only need to mention the implementation of the QUnit start() and stop() functions.

Start()

From QUnit: Start running tests again after the testrunner was stopped. See stop().

Basically, hits play on the test runner suite.

Stop()

From QUnit: Increase the number of start() calls the testrunner should wait for before continuing.

Basically, hits pause on the test runner suite. Keep in mind, for as many times as you call stop(), you must also call start() that many times to continue through the test suite.

Ember's implementation

Basically, asyncStart pauses the QUnit test runner suite using QUnit's stop function and subsequently lets it continue on by calling asyncEnd, which uses QUnit's start function:

Test.QUnitAdapter = Test.Adapter.extend({
  asyncStart: function() {
    stop();
  },
  asyncEnd: function() {
    start();
  },
  exception: function(error) {
    ok(false, Ember.inspect(error));
  }
});

Jasmine Async Control

Jasmine implements Async support a little bit differently than QUnit. The easiest way to show this is a simple Jasmine test:

var AsyncTest = {
  counter: 0,
  complete: false,

  testAsync: function() {
    AsyncTest.counter++;
    AsyncTest.complete = true;
    console.log("Async test method complete");
  },

  isAsyncComplete: function() {
    console.log("isAsyncComplete", AsyncTest.complete);
    return AsyncTest.complete;
  }
};

it("implements jasmine async stuff", function() {
  console.log("start test feature");
  setTimeout(AsyncTest.testAsync, 1000);
  waitsFor(AsyncTest.isAsyncComplete, "test async to have run", 5000);

  runs(function(){
    console.log("runs block post waitFor asyncComplete");
    expect(AsyncTest.counter).toEqual(1);
  });
});

The two important functions in Jasmine when using Async control are waitsFor and runs. waitsFor takes a function as an argument (in my case isAsyncComplete) and polls that function as many times as it can before it times out waiting for it to return true. The default timeout is 5 seconds, but you can bump that up as long as you'd like.

Once the latch function (isAsyncComplete) returns true, Jasmine will run any logic inside its runs function. Keep in mind, any logic (read: assertions) you want to wait for async activity, must be wrapped in the runs function. If it is not, Jasmine will not wait.

Take a look at the console output from my above test in the bullets below:

  • start test feature helpers_spec.js:76
  • isAsyncComplete false helpers_spec.js:70 (90 times)
  • Async test method complete helpers_spec.js:66
  • isAsyncComplete true helpers_spec.js:70
  • runs block post waitFor asyncComplete

I set the testAsync method to wait 1 second before running. In that time, Jasmine polled isAsyncComplete 90 times asking if it was finished. Once it was done, it triggered running my assertion which correctly asserted the counter was at 1.

Onto the Jasmine Ember.Test.adapter.

Jasmine Ember Test.Adapter

This adapter currently lives here on my GitHub. My implementation is as simple as:

Ember.Test.JasmineAdapter = Ember.Test.Adapter.extend({
  asyncRunning: false,

  asyncStart: function() {
    Ember.Test.adapter.asyncRunning = true;
    waitsFor(Ember.Test.adapter.asyncComplete);
  },

  asyncComplete: function() {
    return !Ember.Test.adapter.asyncRunning;
  },

  asyncEnd: function() {
    Ember.Test.adapter.asyncRunning = false;
  },

  exception: function(error) {
    expect(Ember.inspect(error)).toBeFalsy();
  }
});

I keep an asyncRunning property on my JasmineAdapter Ember.Object. To pause the Jasmine suite, I set that property to true and set up my Jasmine waitsFor function to poll asyncComplete until asyncRunning is false or it times out.

Once asyncEnd is called internally by Ember's wait function, asyncRunning will be set to false and the next time Jasmine asks, it will know it's safe to carry on.

The last piece is actually setting the Ember test adapter to be our Jasmine adapter, which happens for us in our Jasmine spec_helper.js:

Ember.Test.adapter = Ember.Test.JasmineAdapter.create();

Wrap up

This adapter is just one implementation, but I haven't seen another out there yet. There's still some work to be done (see TODOs), so please feel free to share any comments, questions, etc. -- as always, contributions welcome.

Hopefully this at least helps remove some of the burden of using Jasmine testing in your Ember apps. Salud.