Last time we took a quick look at setting up Jasmine, one of the popular testing frameworks for Javascript, in your Node project. Today we're going to go a bit deeper on how tests are written and how to handle things like asynchronous behaviour. This one might get a little lengthy, so lets jump right in.  


Let's start off with a simple sample test suite, to get a bit of a feel for how we write tests with Jasmine. Test suites in Jasmine are defined with the describe keyword, and take two parameters, a String for the suite name and a function to implement it. Within the describe blocks our individual tests are implemented with the it keyword. It similarly takes a String, which defines the spec name, and a function where we implement the logic for our test. Jasmine comes packed with a lot of matchers that we can make use of in our test cases, let's check out a few of them below:

userSpec.js

describe("User Suite", function() {

  
// ====================
// A bit of set-up
// ====================
  
  var user = {
    name: 'Santa Claus',
    location: 'North Pole',
    favoriteThings: ['Milk', 'Cookies', 'Rudolph'],
    setLocation = function(value){
        location = value;
    }  
  };
  
  beforeEach(function() {
    spyOn(user, 'setLocation').and.callThrough();
    setLocation('Canada');
  });       


// ====================
// Our tests
// ====================

  it("Should exist", function() {
    expect(user).toEqual(jasmine.anything());
  });
  
  it("Should have a user named Santa Claus", function() {
    expect(user.name).toBe('Santa Claus');
  });
  
  it("Should have Cookies as one of the favoriteThings", function() {
    expect(user.favoriteThings).toContain('Cookies');
  });
  
  it("Should not have a property dislikesChildren", function() {
    expect(user.dislikesChildren).toBeUndefined();
  });

  it("Should be able to call its functions", function() {
    expect(user.setLocation).toHaveBeenCalled();
  });

  it("Should have a working setLocation function", function() {
    expect(user.location).toBe('Canada');
  });
  
});

The full list of matchers can be found in the Jasmine Documentation, a few we're highlighting here include: 

.toEqual()

Typically used to check simple literals and variables, it can also be used to check for object equality between two objects. Here we are making use of the special jasmine.anything(), which will return true if the actual value is anything other than null or undefined, to check the existence of our user object.

.toBe() 

Makes use of '===' under the hood to check equality between the expected value and the actual value.

.toContain() 

Checks the contents of an Array to find if the expected value is contained in that array, returns true if it's found.

.toBeUndefined()

Will return true if the value is undefined, like in our case, looking for an incorrect property on an object.

.toHaveBeenCalled()

One of the many uses of function spies, will return true if the provided function was called during program execution. This includes calls to the double of that function. As a side note, the .and.callThrough() method is important to include on our function spies if we are testing the functionality of a particular call, it indicates to Jasmine to go ahead and execute the actual function we're referencing, rather than the double. If for example our beforeEach function omitted the call through:

beforeEach(function(){
  spyOn(user, 'setLocation');
  setLocation('Canada');
});

Then our final test case, 'Should have a working setLocation function', would fail, Jasmine creates a double of the function to test against, not the actual function itself and our location would remain 'North Pole' rather than 'Canada'.


So simple instances where we have control over the program flow are all well and good, but what about when we have to deal with asynchronous behaviour? Lets say for example we aren't using a direct user object like we have above but are instead making an API call to pull that data object from another source.  

var user = {};
user = someApiCall();

If there is a delay on when we get the data to populate our user object, Jasmine is going to start throwing a lot of test failures when it just keeps chugging along rather than waiting for our API response. Thankfully there is a solution to this, the special callback done(). It will indicate to Jasmine that it should wait (by default 5 seconds, though you can adjust this if need be) for our spec to finish before it throws any errors. Let's take a look at an example with promises:

describe("Async User Suite", function () {
  
  var user = {};

  beforeEach(function (done) {
    $.getJSON("http://localhost/8080/api/users/")
    .done(function (result) {
      user = result;
      
      // Invoke Jasmine's done callback
      done();
    });
  });

  it("Should have our user object", function () {
      expect(user).not.toBeUndefined();
      expect(user.name).toBe('Santa Claus');
  });

});

This time, assuming our API doesn't take forever, our tests will succeed. We are using jQuery for our promises, but you can use any method you'd like, the important element is to invoke done() when you are finished with your data / are at the point where you want to test. There is actually a bit of a gotcha using the jQuery method in that the success method for $.getJSON is also named done, the .done(function (result){ ... }); method above. Just something to watch out for.

The last thing that could probably use mentioning is actually running our tests. You can run your test suites simply by running jasmine from the command line in the root directory of your project. You can also give it a relative path to the suite you wish to run if you have a larger test set and don't want to run them all. For example: jasmine spec/userSpec.js will just run our userSpec file. Plugins are available for all major task-runners and bundlers like Grunt, Gulp and Webpack to help with automating this as well. 


And that's our overview of Jasmine, a relatively simple yet powerful library for testing your Javascript. Next up is another two-parter (maybe three depending on how large it gets) on building an API using Node.js and MongoDB to add a little cross-platform data freedom to your projects. 

-Brad