Unit testing and integration testing Meteor applications with BDD using Velocity and Jasmine

UPDATE (22/09/2014) For the updated version of this tutorial for Meteor > 0.9.0 go here

This tutorial concerns Meteor applications Please note that this is a first version of the tutorial, so it will contain a lot of my comments in this format, in which I ask community to help me answer to my questions. Also, some grammatical errors may appear, for which I apologise.

Prologue

In our application students can register for their tutorials. Tutorials have a name and a maximum capacity. Students cannot register if tutorials have reached a maximum capacity. Also, students have to be logged in to register for the tutorial. Tutorials are added to the system by admins, that is users with a role “admin”. Admins cannot delete tutorials with active registrations.

For demonstration purposes, we have decided to keep some functionality on client, and some on servers. We will take the BDD way of developing our application. For testing we will use jasmine-unit and jasmine packages. You may ask: “But what’s the difference?”.

  • Jasmine-unit package is focused strictly on unit testing, running in its own context, where most of the functionality has been stubbed, therefore you can fully focus on testing your code. Remember: With unit tests you test only what’s yours and assume that what is not your just works!. Please note that all console.logs of jasmine-unit tests can only be seen in the system terminal.
  • Jasmine package, runs in the context of the application, preserving all functionality. Therefore it can be used for integration testing, such as testing of template rendering, method calls and more. Please note that all console.logs of jasmine appear in the browser’s javascript console.

We will use these packages in the following order:

  1. Develop server functionality for admins with unit tests using (jasmine-unit)
  2. Develop client functionality for students, create view model with unit tests using (jasmine-unit)
  3. Write integration tests for template rendering using (jasmine).
  4. Create the template for students and admins and write unit tests for event functionality (jasmine-unit)

QUESTION: Is really template rendering an integration test? In my opinion it still belong to unit testing, although with access to full application. What would be the good example of an integration test?

Chapter 1: Preparing Application

We start by creating an empty Meteor application and adding all necessary packages needed to run our application. If you have downloaded our code from GitHub, you can skip this whole chapter.

$ meteor create Tutorials
$ cd Tutorials
$ rm Tutorials*

Configure Meteor app and add standard Meteor packages

$ meteor remove autopublish
$ meteor remove insecure
$ meteor add accounts-password

Add Meteorite packages for routing, role management and of course testing. In our application, we use jasmine-unit package for unit testing, jasmine package for the integration testing and selenium-nightwatch for acceptance testing!.
\n

$ mrt add iron-router
$ mrt add roles
$ mrt add jasmine-unit
$ mrt add jasmine
$ mrt add mocha-web-velocity
$ mrt add velocity-html-reporter
$ mrt add spin
$ mrt add bootstrap-3
$ mrt add accounts-ui-bs3-and-blaze

REMARK: We have also added Mocha framework, which we will not use, but for some reason velocity is not picking up jasmine (not jasmine-unit) tests when mocha is not present and it’s stuck on “loading”. It is a possible bug.

Now, we create standard directories in our application:

$ mkdir tests
$ mkdir tests/jasmine
$ mkdir tests/jasmine/client
$ mkdir client
$ mkdir server
$ mkdir lib

Basic configuration is ready, we now proceed with the preparation of server model.

Chapter 2: Server

In this chapter, we focus on delivering the server functionality to our app. We follow the specification of the functionality of our application. We start with the initial requirement of our model: Admin can create tutorial.

This tutorial follows the triangulation principle of TDD and BDD, with red-green-refactor approach. Meaning:

  1. We write a failing test which turns the test suite “red”.
  2. We make the test pass with the simplest implementation possible, turning the test suite “green”
  3. We refactor the existing code, repairing broken tests and repeat this process.

First, we place a new file inside the tests folder named tutorial-jasmine-unit.js. We have to respect the *-jasmine-unit.js notation, in order for tests to be picked up by the jasmine-unit package. You can change this naming inside smart.json file of the jasmine-unit package.

// file: tests/tutorial-jasmine-unit.js
(function () {
    "use strict";
    describe("Tutorial", function () {
        it("should be created with name and capacity", function () {
            spyOn(Tutorials, "insert").andReturn(1);

            Meteor.methodMap.createTutorial("Tutorial 1", 20);

            expect(Tutorials.insert).toHaveBeenCalledWith({name: "Tutorial 1", capacity: 20});
        });
    });
})();

It’s quite a lot that we have covered with this test. We have assumed the structure of the Tutorial collection having “name” and “capacity”. Also, we have assumed existence of the “createTutorial” method and that inside this method we call insert to the assumed collection Tutorials.

QUESTIONS:
1. Is it correct to test server methods with: Meteor.methodMap.*?
2. Is it possible to decompose this more and not taking such a huge step?

When we run the meteor application with $ mrt command we will get the famous red dot.

Red dot

Test error says Failed: ReferenceError: Tutorials is not defined. It is time to define the collection and start implementing the server functionality:

//file: lib/collections.js
Tutorials = new Meteor.Collection("tutorials")

After defining the collection, we are still in red with following error: Failed: TypeError: Object has no method ‘createTutorial’. Simply put, the createTutorial method is yet missing. So let’s create it!

//file: server/tutorials.js
Meteor.methods({
    createTutorial: function() {
    }
})

Still in red (obviously!), now with the following error: *Failed: Expected spy insert to have been called with [ { name : ‘Tutorial 1’, capacity : 20 } ] but it was never called. *. We need to implement the body of the function.

//file: server/tutorials.js
Meteor.methods({
    createTutorial: function(name, capacity) {
        Tutorials.insert({name: name, capacity: capacity});
    }
})

Finally in green! Now we can proceed with making this method call secure using role based assess using the Roles package. We write a following test:

it("should not be created by non-admins", function () {
    spyOn(Meteor, "user").andReturn({});
    spyOn(Roles, "userIsInRole").andReturn(false);
    spyOn(Tutorials, "insert");

    expect(Meteor.methodMap.createTutorial).toThrow();
    expect(Tutorials.insert).not.toHaveBeenCalled();
    expect(Roles.userIsInRole).toHaveBeenCalledWith({}, "admin");
});

In this test we spy on several functions. First, we want our method to check for user and his role, returning mocked values, simulating authorisation for admin. This test will fail since we miss the implementation of the functionality:

createTutorial: function(name, capacity) {
    if (!Meteor.user() || !Roles.userIsInRole(Meteor.user(), "admin")) {
        throw new Meteor.Error(403, "Access Denied");
    }

    Tutorials.insert({name: name, capacity: capacity});
}

We are in green!

But oops, this broke our first test! This is the result of not very fortunate design of our first test, which should be decomposed a bit better.

Since this breaks our first test, we adjust it by adding following spies:

it("should be created with name and capacity", function () {
    spyOn(Tutorials, "insert").andReturn(1);
    spyOn(Meteor, "user").andReturn({});
    spyOn(Roles, "userIsI
    ...

For test completion we also add inverse test (we can make it first red by returning false in the mocked ):

it("should be created only by admins", function () {
    spyOn(Meteor, "user").andReturn({});
    spyOn(Roles, "userIsInRole").andReturn(true);
    spyOn(Tutorials, "insert");

    expect(Meteor.methodMap.createTutorial).not.toThrow();
    expect(Tutorials.insert).toHaveBeenCalled();
    expect(Roles.userIsInRole).toHaveBeenCalledWith({}, "admin");
});

We follow the same approach and create tests and implementation for update and delete of Tutorials. Following is a complete listing of tests of the server functionality. Please note the refactoring we have done replacing spy with mock for Meteor.user(). Also we have joined the first two Tutorial creation tests into one, testing tutorial creation and security access.

// file: tests/tutorial-jasmine-unit.js
(function () {
    "use strict";
    describe("Tutorial", function () {
        // mock
        Meteor.user = function() { return {} };

        it("should be created by admins with name and capacity", function () {
            spyOn(Tutorials, "insert").andReturn(1);
            spyOn(Roles, "userIsInRole").andReturn(true);

            Meteor.methodMap.createTutorial("Tutorial 1", 20);

            expect(Tutorials.insert).toHaveBeenCalledWith({name: "Tutorial 1", capacity: 20});
            expect(Roles.userIsInRole).toHaveBeenCalledWith({}, "admin");
        });

        it("should not be created by non-admins", function () {
            spyOn(Roles, "userIsInRole").andReturn(false);
            spyOn(Tutorials, "insert");

            expect(Meteor.methodMap.createTutorial).toThrow();
            expect(Tutorials.insert).not.toHaveBeenCalled();
        });

        it("should be able to update its name and capacity by admins", function () {
            spyOn(Roles, "userIsInRole").andReturn(true);
            spyOn(Tutorials, "update");

            Meteor.methodMap.updateTutorial(1, "Tutorial 1", 20);

            expect(Tutorials.update).toHaveBeenCalledWith(1, {$set: { name: "Tutorial 1", capacity: 20 }});
            expect(Roles.userIsInRole).toHaveBeenCalledWith({}, "admin");
        });

        it("should not be updated by non-admins", function () {
            spyOn(Roles, "userIsInRole").andReturn(false);
            spyOn(Tutorials, "update");

            expect(Meteor.methodMap.createTutorial).toThrow();
            expect(Tutorials.update).not.toHaveBeenCalled();
            expect(Roles.userIsInRole).toHaveBeenCalledWith({}, "admin");
        });

        it("should be possible to delete tutorial by admins", function () {
            spyOn(Roles, "userIsInRole").andReturn(true);
            spyOn(Tutorials, "remove");

            Meteor.methodMap.removeTutorial("1");

            expect(Tutorials.remove).toHaveBeenCalledWith("1");
            expect(Roles.userIsInRole).toHaveBeenCalledWith({}, "admin");
        });

        it("should not be possible to delete tutorial by non-admins", function () {
            spyOn(Roles, "userIsInRole").andReturn(false);
            spyOn(Tutorials, "remove");

            expect(Meteor.methodMap.removeTutorial).toThrow();
            expect(Tutorials.remove).not.toHaveBeenCalled();
        });

    });
})();
//file: server/tutorials.js
Meteor.methods({
    createTutorial: function(name, capacity) {
        if (!Meteor.user() || !Roles.userIsInRole(Meteor.user(), "admin")) {
            throw new Meteor.Error(403, "Access Denied");
        }
        Tutorials.insert({name: name, capacity: capacity});
    },
    updateTutorial: function(id, name, capacity) {
        if (!Meteor.user() || !Roles.userIsInRole(Meteor.user(), "admin")) {
            throw new Meteor.Error(403, "Access Denied");
        }
        Tutorials.update(id, {$set: {name: name, capacity: capacity}});
    },
    removeTutorial: function(id) {
        if (!Meteor.user() || !Roles.userIsInRole(Meteor.user(), "admin")) {
            throw new Meteor.Error(403, "Access Denied");
        }
        Tutorials.remove(id);
    }
});

Looking at the specification we are still missing one more business requirement: Admins cannot delete tutorials with active registrations. So let’s write a new test:

it("should not be possible to delete tutorial with active registrations", function () {
    spyOn(Roles, "userIsInRole").andReturn(true);
    spyOn(Tutorials, "remove");
    spyOn(TutorialRegistrations, "find").andReturn({count: function() { return 2 }});

    try
    {
        Meteor.methodMap.removeTutorial("1");
    }
    catch (ex) {
        expect(ex).toBeDefined();
    }

    expect(Meteor.methodMap.removeTutorial).toThrow();
    expect(TutorialRegistrations.find).toHaveBeenCalledWith({tutorialId: "1"});
    expect(Tutorials.remove).not.toHaveBeenCalled();
});

We have decided to keep registrations in a separate collection TutorialRegistrations. Therefore we need to add this collection to our application and extend the functionality of removeTutorial function.

//file: lib/collections.js
TutorialRegistrations = new Meteor.Collection("tutorialRegistrations");
//file: server/tutorials.js
...
removeTutorial: function(id) {
    if (!Meteor.user() || !Roles.userIsInRole(Meteor.user(), "admin")) {
        throw new Meteor.Error(403, "Access Denied");
    }

    if (TutorialRegistrations.find({tutorialId: id}).count() > 0) {
        throw new Meteor.Error(406, "Tutorial has registrations");
    }
    Tutorials.remove(id);
}

All green! Looks like we’re done on server, let’s move to client.

Chapter 3: Client model

In this chapter we create a client view model, which will facilitate its rendering in the template, as well as handle communication with the server. This model includes functionality from both client and server. So let’s get right to it and write tests that handle model specification. We follow the same triangulation (ref-green-refactor) approach as in server, so I hope this part does not need much explication.

First we solve the initialisation:

// file: tests/tutorialViewModel-jasmine-unit.js
(function () {
    "use strict";
    describe("TutorialViewModel", function () {
        it("Should have id, name and capacity", function() {
            var model = TutorialViewModel.create("1", "Name", 10);
            expect(model._id).toEqual("1");
            expect(model.name).toEqual("Name");
            expect(model.capacity).toEqual(10);
        });
    })
})();

And here is the model file:

//file: client/models/tutorialViewModel.js
TutorialViewModel = {
    _id: null,
    name: null,
    capacity: 0,
    init: function(id, name, capacity) {
        this._id = id;
        this.name = name;
        this.capacity = capacity;
        return this;
    },
    create: function(id, name, capacity) {
        return Object.create(TutorialViewModel).init(id, name, capacity);
    }
};

Now, we implement saving, so let’s write couple tests for object creation:

// file: tests/tutorialViewModel-jasmine-unit.js
...
it("Should not save when name is not defined", function() {
    var model = TutorialViewModel.create(null, "", 10);
    expect(model.save).toThrow();
});

it("Should not save when capacity is not defined", function() {
    var model = TutorialViewModel.create(null, "Name", 0);
    expect(model.save).toThrow();
});

it("Should save when initialised properly", function() {
    var model = TutorialViewModel.create(null, "Name", 10);

    // create spy for alert
    Object.prototype.alert = function() {};
    spyOn(Object.prototype, "alert");

    spyOn(Meteor, "call").andCallFake(function (functionName, name, capacity, callback) {

        callback("Error", null);
        expect(alert).toHaveBeenCalled();

        callback(null, "1");
        expect(model._id).toEqual("1");
    });

    model.save();

    expect(Meteor.call).toHaveBeenCalled();
    expect(Meteor.call.mostRecentCall.args[0]).toEqual("createTutorial");
    expect(Meteor.call.mostRecentCall.args[1]).toEqual("Name");
    expect(Meteor.call.mostRecentCall.args[2]).toEqual(10);
});

Whoa, whoa, whoa! What just has happened? We did a LOT in this test. First, we have added “alert” function into global namespace since in jasmine-unity it is mocked away. Then, we have created a spy for the Meteor.call in which we test both positive and a negative response. In case of negative response , alert is displayed. In case of positive response new _id is assigned to the model. Last expectations check if Meteor.call method has been called with correct arguments. Following is the implementation of the save function, which makes this test green.

//file: client/models/tutorialViewModel.js
...
save: function() {
    if (!this.name) {
        throw "Name empty or not defined!";
    }
    if (!this.capacity) {
        throw "Capacity 0 or not defined";
    }

    var that = this;
    Meteor.call("createTutorial", this.name, this.capacity, function(err, id) {
        console.log(err + ":" + id);
        if (err) {
            alert(err);
        } else {
            that._id = id;
        }
    });
}

It’s time to extend the functionality of the view model with possibility to save updated changes to the server. Decision whether to insert or update depends on whether _id is defined. First, let’s write the test.

// file: tests/tutorialViewModel-jasmine-unit.js
it("Should save updated changes", function() {
    var model = TutorialViewModel.create("1", "Name", 10);

    spyOn(Object.prototype, "alert");
    spyOn(Meteor, "call").andCallFake(function (functionName, id, name, capacity, callback) {
        callback("Error", null);
        expect(alert).toHaveBeenCalled();
    });

    model.save();

    expect(Meteor.call).toHaveBeenCalled();
    expect(Meteor.call.mostRecentCall.args[0]).toEqual("updateTutorial");
    expect(Meteor.call.mostRecentCall.args[1]).toEqual("1");
    expect(Meteor.call.mostRecentCall.args[2]).toEqual("Name");
    expect(Meteor.call.mostRecentCall.args[3]).toEqual(10);
});

Test is in red, since our save method currently only supports saving a new tutorial. Let’s update the implementation.

//file: client/models/tutorialViewModel.js
save: function() {
    if (!this.name) {
        throw "Name empty or not defined!";
    }
    if (!this.capacity) {
        throw "Capacity 0 or not defined";
    }

    var that = this;

    if (this._id) {
        Meteor.call("updateTutorial", this._id, this.name, this.capacity, function(err) {
            if (err) {
                alert(err);
            }
        });
    } else {
        Meteor.call("createTutorial", this.name, this.capacity, function(err, id) {
            if (err) {
                alert(err);
            } else {
                that._id = id;
            }
        });
    }
}

We follow the same approach to implement the functionality for delete operation. Following are tests for the delete function (we triangulate them one by one).

// file: tests/tutorialViewModel-jasmine-unit.js
it("should delete the existing tutorial", function() {
    var model = TutorialViewModel.create("1", "Name", 10);
    spyOn(Object.prototype, "alert");
    spyOn(Meteor, "call").andCallFake(function (functionName, id, callback) {
        callback("Error", null);
        expect(alert).toHaveBeenCalled();
    });

    model.delete();

    expect(Meteor.call).toHaveBeenCalled();
    expect(Meteor.call.mostRecentCall.args[0]).toEqual("removeTutorial");
    expect(Meteor.call.mostRecentCall.args[1]).toEqual("1");
});

it("should not delete a new tutorial", function() {
    var model = TutorialViewModel.create(null, "Name", 10);
    spyOn(Meteor, "call");

    model.delete();

    expect(Meteor.call).not.toHaveBeenCalled();
});

And here is the implementation that makes these tests green.

//file: client/models/tutorialViewModel.js
delete: function() {
    if (!this._id) return;

    Meteor.call("removeTutorial", this._id, function(err) {
        if (err) {
            alert(err);
        }
    });
}

Since we have covered all the functionality of Admin, we now proceed to functionality for students. Our first requirement is that students can register for a tutorial which is not over its capacity. We will include all the tutorial related functionality in the view model. Let’s write the unit test.

it("should allow students to register for the tutorial", function() {
    var model = TutorialViewModel.create("1", "Name", 10);
    var studentId = "2";

    spyOn(TutorialRegistrations, "find").andReturn({count: function() { return 8; }});
    spyOn(TutorialRegistrations, "insert");

    model.registerStudent(studentId);

    expect(TutorialRegistrations.insert).toHaveBeenCalled();
    expect(TutorialRegistrations.insert.mostRecentCall.args[0]).toEqual({ tutorialId : '1', studentId : '2' });
});

and the implementation

//file: client/models/tutorialViewModel.js
registerStudent: function(studentId) {
        if (TutorialRegistrations.find({tutorialId: this._id}).count() >= this.currentCapacity) {
            throw "Capacity of the tutorial has been reached!";
        }
        TutorialRegistrations.insert({tutorialId: this._id, studentId: studentId}, function (err, id) {
            if (!err) {
                this.currentCapacity += 1;
            }
        });
    }

While making this test, we found that this design has flaw in having to publish all registrations to client. We realise that it is a good idea to add a bit of redundant information to the tutorial about the number of registrations it currently has active. When a new registration is added or removed, this number is changed. Since we have decided to keep all the tutorial functionality on server, we need to add a new server method. While at it, we add also the method for removing the registration. Following is a list of tests we used during implementation of server functionality

// file: tests/tutorial-jasmine-unit.js
it("should not be possible to register while at maximum capacity", function() {
    spyOn(Tutorials, "find").andReturn({capacity: 1, currentCapacity: 1});
    expect(Meteor.methodMap.registerForTutorial).toThrow();
});

it("should not be possible to register if registration is present", function() {
    spyOn(Tutorials, "find").andReturn({capacity: 2, currentCapacity: 1});
    spyOn(TutorialRegistrations, "findOne").andReturn({});

    expect(Meteor.methodMap.registerForTutorial).toThrow();
});

it("should be possible to register if within capacity and first registration", function() {
    spyOn(Meteor, "userId").andReturn("2");
    spyOn(Tutorials, "findOne").andReturn({capacity: 2, currentCapacity: 1});
    spyOn(TutorialRegistrations, "findOne").andReturn(null);
    spyOn(TutorialRegistrations, "insert");
    spyOn(Tutorials, "update");

    Meteor.methodMap.registerForTutorial("1");

    expect(TutorialRegistrations.insert).toHaveBeenCalledWith({tutorialId: "1", userId: "2"});
    expect(Tutorials.update).toHaveBeenCalledWith("1", {$set: { currentCapacity: 2}});

});

it("should not be possible to de-register if registration not present", function() {
    spyOn(TutorialRegistrations, "findOne").andReturn();
    expect(Meteor.methodMap.removeRegistration).toThrow();
});

it("should be possible to de-register if registration exists", function() {
    spyOn(Meteor, "userId").andReturn("2");
    spyOn(TutorialRegistrations, "findOne").andReturn({});
    spyOn(TutorialRegistrations, "remove");
    spyOn(Tutorials, "findOne").andReturn({capacity: 2, currentCapacity: 1});
    spyOn(Tutorials, "update");

    Meteor.methodMap.removeRegistration("1");

    expect(TutorialRegistrations.remove).toHaveBeenCalledWith({tutorialId: "1", userId: "2"});
    expect(Tutorials.update).toHaveBeenCalledWith("1", {$set: { currentCapacity: 0}});
});

and following is the implementation (make sure you are editing correct files!)

//file: server/tutorials.js
registerForTutorial: function(tutorialId) {
    var userId = Meteor.userId();
    var tutorial= Tutorials.findOne(tutorialId);
    var tutorialRegistration = TutorialRegistrations.findOne({tutorialId: tutorialId, userId: userId});

    if (tutorial.currentCapacity >= tutorial.capacity) {
        throw new Meteor.Error(406, "Tutorial at full capacity");
    }

    if (tutorialRegistration != null) {
        throw new Meteor.Error(406, "Student already registered");
    }

    Tutorials.update(tutorialId, { $set: { currentCapacity: tutorial.currentCapacity + 1}});
    TutorialRegistrations.insert({tutorialId: tutorialId, userId: userId});
},
removeRegistration: function(tutorialId) {
    var userId = Meteor.userId();
    var tutorial= Tutorials.findOne(tutorialId);
    var tutorialRegistration = TutorialRegistrations.findOne({tutorialId: tutorialId, userId: userId});

    if (tutorialRegistration == null) {
        throw new Meteor.Error(406, "Student not registered for this tutorial");
    }

    TutorialRegistrations.remove({tutorialId: tutorialId, userId: userId});
    Tutorials.update(tutorialId, { $set: { currentCapacity: tutorial.currentCapacity - 1 }});
}

With server functionality in place, we can proceed with the view model. For simplicity we remove tests calling alerts.

it("should register student for the tutorial", function() {
    var model = TutorialViewModel.create("1", "Name", 10);
    spyOn(Meteor, "call");

    model.registerStudent();

    expect(Meteor.call).toHaveBeenCalled();
    expect(Meteor.call.mostRecentCall.args[0]).toEqual("registerForTutorial");
    expect(Meteor.call.mostRecentCall.args[1]).toEqual("1");
});

it("should de-register student from the tutorial", function() {
    var model = TutorialViewModel.create("1", "Name", 10);
    spyOn(Meteor, "call");

    model.removeRegistration();

    expect(Meteor.call).toHaveBeenCalled();
    expect(Meteor.call.mostRecentCall.args[0]).toEqual("removeRegistration");
    expect(Meteor.call.mostRecentCall.args[1]).toEqual("1");
});

This is the implementation of the view model:

registerStudent: function() {
    Meteor.call("registerForTutorial", this._id, function(err) {
        if (err) {
            alert(err);
        }
    });
},
removeRegistration: function() {
    Meteor.call("removeRegistration", this._id, function(err) {
        if (err) {
            alert(err);
        }
    });
}

Looking at the specification of our system, we should be done with the client and server functionality. Let’s start with templates!

Chapter 4: Templates

Testing templates consists of two difference processes. With unit tests we test template behaviours such as template hooks and events. With *integration tests we test template rendering and the behaviour of HTML controls.

First, we setup (iron) router with all the routes (this is done incrementally during the development). We setup three different routes. First, the welcome route, accessible to all users. Then, route for authorised students, where they can register for tutorials. And the last route for admins, where they can administer tutorials. We also add some standard templates for showing the loading process or page not found. In the router configuration, we also add hooks for security.

//file:lib/router.js
Router.configure({
    layoutTemplate: 'layout',
    loadingTemplate: 'loading',
    notFoundTemplate: 'template404',
    waitOn: function() {
        //return Meteor.subscribe('practicals');
    }
});

Router.map(function() {
    this.route('introduction', {
        path: '/'
    });
});

Router.map(function() {
    this.route('tutorials', {
        path: '/tutorials',
        waitOn: function() {
            return [Meteor.subscribe('tutorials'), Meteor.subscribe('registrations')];
        }
    });
});

Router.map(function() {
    this.route('admin', {
        path: '/admin',
        waitOn: function() {
            return Meteor.subscribe('tutorials');
        }
    });
});

Router.onBeforeAction('loading');

// security (check the full file on [GitHub](https://github.com/tomitrescak/TestingMeteor))
...

QUESTION: How to properly test if routing works? Is this part of integration or acceptance testing?

The waitOn parameter of the route allows Meteor application to pause render until all data from subscription are made available on client. Adding the “loading” hook renders the “loading” template. In order for subscriptions to work, we need to add server publications.

//file:server/publications.js
Meteor.publish('tutorials', function() {
    return Tutorials.find();
});

Meteor.publish('registrations', function() {
    return TutorialRegistrations.find({userId: this.userId});
});

Now we are ready to start developing our templates. Following is our layout template, this template requires no testing (stored in /client/views/application/layout.html).

<br />    {{> header}}
    {{> yield}}

Header Template

First template that requires testing is the header template. We need to make sure that link for admins only appears to admins and that link for tutorials appears only to registered users. This is our first integration test that we will write.

QUESTION: I present two different versions of the test. First use is using mocked security checkers. Can this still be considered an integration test? In this test we show how to mock the Handlebars helpers isInRole and currentUser. Second version uses no mocks.

Also please note (what REALLY SUCKS!) that jasmine and jasmine-unit use different notation 😦 Looks like jasmine-unit is still stuck in version 1.3 while jasmine uses modern 2.0 notation. Jasmine-unit looks quite solid with velocity, while jasmine has its issues and sometimes refuses to load (see the trick with mocha-web).

Following is the spec file with mocks for the testing of the header template.

//file:/tests/jasmine/client/header-template-spec.js
// this version contains MOCKS
describe("Header template", function() {
    it("should not show tutorial link to anonymous user", function () {
        spyOn(UI._globalHelpers, "currentUser").and.returnValue(false);

        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        expect($(div).find("#tutorialsLink")[0]).not.toBeDefined();
    });

    it("should show tutorial link to registered user", function () {
        spyOn(UI._globalHelpers, "currentUser").and.returnValue(true);

        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        console.log(div.innerHTML);

        expect($(div).find("#tutorialsLink")[0]).toBeDefined();
    });

    it("should show admin link to admins user", function () {
        spyOn(UI._globalHelpers, "isInRole").and.returnValue(true);

        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        expect($(div).find("#adminLink")[0]).toBeDefined();
    });

    it("should not show admin link to non-admins", function () {
        spyOn(UI._globalHelpers, "isInRole").and.returnValue(false);

        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        expect($(div).find("#adminLink")[0]).not.toBeDefined();
    });
});

Following is the spec file without mocks for the testing of the header template. Which one is more correct?. Please note how we login and logout depending on our needs, using asynchronous method calls for login and logout.

//file:/tests/jasmine/client/header-template-nomocks-spec.js
describe("Header template - No Mocks", function() {
    it("should not show tutorial link to anonymous user", function () {
        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        expect($(div).find("#tutorialsLink")[0]).not.toBeDefined();
    });

    it("should be able to login normal user", function (done) {
        Meteor.loginWithPassword('normal@example.com', 'apple1', function (err) {
            expect(err).toBeUndefined();
            done();
        });
    });

    it("should show tutorial link to registered user", function () {
        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        console.log(div.innerHTML);

        expect($(div).find("#tutorialsLink")[0]).toBeDefined();
    });

    it("should be able to logout", function (done) {
        Meteor.logout(function (err) {
            expect(err).toBeUndefined();
            done();
        });
    });

    it("should be able to login normal user", function (done) {
        Meteor.loginWithPassword('admin@example.com', 'apple1', function (err) {
            expect(err).toBeUndefined();
            done();
        });
    });

    it("should show admin link to admins user", function () {
        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        expect($(div).find("#adminLink")[0]).toBeDefined();
    });

    it("should be able to logout", function (done) {
        Meteor.logout(function (err) {
            expect(err).toBeUndefined();
            done();
        });
    });

    it("should not show admin link to non-admins", function () {
        var div = document.createElement("DIV");
        var comp = UI.render(Template.header);

        UI.insert(comp, div);

        expect($(div).find("#adminLink")[0]).not.toBeDefined();
    });
});

And following is the header template itself.

<br /><br /><div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
                <a class="navbar-brand" href="{{pathFor 'introduction'}}">The Great Clara</a>
            </div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
                    {{> loginButtons }} <!-- here -->
                </ul>
<ul class="nav navbar-nav navbar-right">
                    {{#if currentUser}}

<li><a class="brand" href="{{pathFor 'tutorials'}}" id="tutorialsLink">Tutorials</a></li>
                    {{/if}}
                    {{#if isInRole 'admin'}}

<li><a class="brand" href="{{pathFor 'admin'}}" id="adminLink">Administration</a></li>
                    {{/if}}
                </ul>
</div>
            <!--/.nav-collapse -->
        </div>
</div>

Tutorials Template

In tutorials template we list all tutorials or display that there are no tutorials available (please see the use of UI.renderWithData). We display tutorial name, current number of registrations and maximum capacity. Available tutorials are painted with white background, while tutorials with full capacity display with red background. We also display following buttons:

  • Register button is displayed on every available tutorial.
  • Modify button is displayed only for admins on every tutorial
  • Delete tutorial is displayed only for admins on every tutorial which has zero registrations
  • Add tutorial link button is displayed for admins on the bottom on the page

First, we handle template rendering and write integration tests in jasmine. The order of the it specs is given by order of implementation given by the previous specification.

In the first test we check that we render tutorial lines according to the data supplied to the template. Please note the use of UI.renerWithData.

//file:/tests/jasmine/client/tutorials-template-spec.sj
describe("Tutorials template", function() {
    it("should show a list of tutorials when there are some available", function () {
        var div = document.createElement("DIV");
        var data = {tutorials: [{}, {}]};
        data.tutorials.count = function() { return 2; }

        var comp = UI.renderWithData(Template.tutorials, data);

        UI.insert(comp, div);

        expect($(div).find(".tutorialLine").length).toEqual(2);
    });
});

In the second test, within the same file, we check that we display the warning if no tutorials currently exists in the system:

```javascript
it("should show a warning when no tutorials are available", function () {
    var div = document.createElement("DIV");
    var comp = UI.renderWithData(Template.tutorials, {tutorials: {count: function() { return 0; }}});

    UI.insert(comp, div);

    expect($(div).find("#noTutorialsWarning")[0]).toBeDefined();
});

Whenever you want to see how your rendering is doing, you can log it with console.log(div) and see the result in your console.

Next, we want to have all tutorials sorted by title. Creating test for this is going to be a bit tricky, since we want to assign and sort the data in the router and not in the template helper. Here is our test spec proposal:

//file:/tests/jasmine/client/tutorials-template-spec.js
it ("should sort tutorials by name", function() {
    var route = _.findWhere(Router.routes, {name: "tutorials"});
    spyOn(Tutorials, "find").and.returnValue({});

    // call the function "data()"
    var data = route.options.data();

    expect(Tutorials.find).toHaveBeenCalled();
    expect(Tutorials.find.calls.mostRecent().args[0]).toEqual({});
    expect(Tutorials.find.calls.mostRecent().args[1].sort.name).toEqual(1);
    expect(data).toEqual({tutorials: {}});
});

And following is the modification of the router

file:/lib/route.js
Router.map(function() {
    this.route('tutorials', {
        path: '/tutorials',
        waitOn: function() {
            return [Meteor.subscribe('tutorials'), Meteor.subscribe('registrations')];
        },
        data: function() {
            return {tutorials: Tutorials.find({}, {sort: {name: 1}}) };
        }
    });
});

If we would use a template helper to obtain our data, instead of using iron router’s data() function, creating the test would be easier:

//file:/client/views/tutorials/tutorials.js
Template.tutorials.tutorials = function() {
    return Tutorials.find({}, {sort: {name: 1}}
};

//file:/tests/jasmine/client/tutorials-template-spec.js
it ("should sort tutorials by name", function() {
    spyOn(Tutorials, "find").and.returnValue({});

    // call the function "data()"
    var data = Template.tutorials.tutorials();

    expect(Tutorials.find).toHaveBeenCalled();
    expect(Tutorials.find.calls.mostRecent().args[0]).toEqual({});
    expect(Tutorials.find.calls.mostRecent().args[1].sort.name).toEqual(1);
    expect(data).toEqual({tutorials: {}});
});

This is the template we have so far:

<br /><br /><h1>Tutorials</h1>
    {{#if tutorials.count}}
        {{#each tutorials}}

<div class="tutorialLine">Line</div>
        {{/each}}
    {{else}}

<p class="well bg-warning" id="noTutorialsWarning">There are no tutorials available ...

    {{/if}}

Next, we will implement all admin buttons that allow us to create and modify tutorials. We have decided to create only one test case for all buttons. This test is using stubs for authorisation checks.

//file:/tests/jasmine/client/tutorials-template-spec.js
it("should show create, modify and delete button to admin user", function () {
    spyOn(UI._globalHelpers, "isInRole").and.returnValue(true);

    var data = {tutorials: [{}, {}]};
    data.tutorials.count = function() { return 2; };

    var comp = UI.renderWithData(Template.tutorials, data);
    UI.insert(comp, div);

    expect($(div).find("#createTutorial")[0]).toBeDefined();
    expect($(div).find(".modifyTutorial").length).toEqual(2);
    expect($(div).find(".removeTutorial").length).toEqual(2);
});

it("should not show create, modify and delete button to non-admin user", function () {
    spyOn(UI._globalHelpers, "isInRole").and.returnValue(false);

    var data = {tutorials: [{}, {}]};
    data.tutorials.count = function() { return 2; };

    var comp = UI.renderWithData(Template.tutorials, data);
    UI.insert(comp, div);

    expect($(div).find("#createTutorial")[0]).not.toBeDefined();
    expect($(div).find(".modifyTutorial")[0]).not.toBeDefined();
    expect($(div).find(".removeTutorial")[0]).not.toBeDefined();

    console.log(div.innerHTML);
});

And this is the template we have so far:

<h1>Tutorials</h1>
    {{#if tutorials.count}}
        {{#each tutorials}}

<div class="row tutorialLine">
                {{name}} <span class="badge">{{currentCapacity}} / {{capacity}}</span>

                {{#if isInRole 'admin'}}
                    <span class="glyphicon glyphicon-trash"></span> Delete
                    <a class="btn btn-info pull-right modifyTutorial" href="{{pathFor 'admin'}}"><span class="glyphicon glyphicon-edit"></span> Edit</a>
                {{/if}}
            </div>
        {{/each}}
    {{else}}

<p class="well bg-warning" id="noTutorialsWarning">There are no tutorials available ...

    {{/if}}

    {{#if isInRole 'admin'}}

<hr />
        <a href="{{pathFor 'admin'}}" class="btn btn-primary" id="createTutorial">Create Tutorial</a>
    {{/if}}

Looks like our template is ready! Well, not entirely. One of the requests we had, was that admins cannot delete tutorials which have at least one registration. It is a good practice to hide all actions that are not currently possible for clarity. For this we implement a new template helper, starting with the unit test of this helper. For unit tests, we use jasmine-unit.

QUESTION: Do you think that this is good practice? I am quite not sure if my template testing is even “integration” testing.

// file: tests/tutorials-template-jasmine-unit.js
(function () {
    "use strict";
    describe("TutorialTemplate", function () {
        it("function canDelete should return true only when tutorial has no registrations", function () {
            // stub values called with accessor "this.currentCapacity"
            Template.tutorials.currentCapacity = 0;
            expect(Template.tutorials.canDelete()).toBeTruthy();
            // cleanup
            delete Template.tutorials.currentCapacity;
        });
        it("function canDelete should return false when there are registrations", function () {
            // stub values called with accessor "this.currentCapacity"
            Template.tutorials.currentCapacity = 1;
            expect(Template.tutorials.canDelete()).toBeFalsy();
            // cleanup
            delete Template.tutorials.currentCapacity;
        });
    });
})();

These test provide a very good example of how we can stub data for function calls and then simply cleanup those data in order for them not to interfere with tests. The biggest challenge her is to find out where the “this” context is currently set to. In helper methods it is always set to the context of the template. It is time to implement our simple helper method to make this test green.

Template.tutorials.helpers({
    canDelete: function() {
        console.log(this);
        return this.currentCapacity == 0;
    }
});

We can now use this helper in our tutorials template to hide all delete buttons and prohibit deleting tutorials with active registrations. We write following test, in which we pass data with two tutorials, from which only one should be able to delete:

//file:/tests/jasmine/client/tutorials-template-spec.js
it("should not show delete button for tutorials with active registrations", function () {
    spyOn(UI._globalHelpers, "isInRole").and.returnValue(true);

    var data = {tutorials: [{currentCapacity: 1}, {currentCapacity: 0}]};
    data.tutorials.count = function() { return 2; };

    var comp = UI.renderWithData(Template.tutorials, data);
    UI.insert(comp, div);

    expect($(div).find(".removeTutorial").length).toEqual(1);
});

We adjust the template accordingly using the `{{#if canDelete}}…{{/if}}. This makes our test green, but breaks another test, in which we test if all admin buttons are displayed. We adjust this test to include information on the capacity of data:

it("should show create, modify and delete button to admin user", function () {
    spyOn(UI._globalHelpers, "isInRole").and.returnValue(true);

    var data = {tutorials: [{currentCapacity: 0}, {currentCapacity: 0}]};
...

Following the very same approach we add the button for student registration for tutorials. This button is visible for tutorials which have available capacity and student is not yet registered for them. To achieve this we write a unit test for the helper method and an integration test for testing the button visibility (Since there is nothing new to show, please see the complete code on GitHub).

Phew! So far we have covered a lot, and yet this is still not the end! Currently, we have a working client and server functionality and our templates render what we need. Yet we miss all the page interactivity. Therefore, we need to add and test template events. We contemplate the tutorial registration event, the rest can be found on GitHub. Let’s start with our unit test.

In this test, we cover clicking on the button with class “.registerForTutorial”, which should trigger call of the registerStudent() from our view model. Here it goes:

// file: tests/tutorials-template-jasmine-unit.js
it("should be able to register for tutorial by clicking on the '.registerForTutorial' button", function () {
    Object.prototype.registerStudent = function() {};
    spyOn(Object.prototype, "registerStudent");
    Template.tutorials.fireEvent("click .registerForTutorial", {preventDefault: function() {}});
    expect(Object.prototype.registerStudent).toHaveBeenCalled();
    delete Object.prototype.registerStudent;
});

We know, that in helper we will be calling the this.registerStudent(). Since in unit test the context of this varies from the real application, the safest way to stup and test this action is by adding it to the Object. Now, to make this test green, we implement it like this:

//file:/client/views/tutorials/tutorials.js
Template.tutorials.events({
    "click .registerForTutorial": function(e) {
        e.preventDefault();
        this.registerStudent();
    }
});

But, this has a major flaw! Context of “this” is within cursor of the Tutorials collection. In our template we browse lines of the collection directly, we do not instantiate our model. We need to adjust this by creating a new helper:

//file:/client/views/tutorials/tutorials.js
Template.tutorials.helpers({
    ...
    tutorialModel: function() {
        return TutorialViewModel.create(this._id, this.name, this.capacity, this.currentCapacity);
    }
});

and adjusting our template

<br /><br /><h1>Tutorials</h1>
    {{#if tutorials.count}}
        {{#each tutorials}}
            {{#with tutorialModel}}
            ...

QUESTION: Does this look like an overkill to you? I really like to have my view model with all the functionality inside, since it very much simplifies my testing workflow.

With this, we have exhausted all the examples we could draw during development of our applications. Please download the full code from GitHub.

Chapter 5: Acceptance Tests with Selenium-Nightwatch

I have decided to make this tutorial “complete” by adding information on how to add and run acceptance tests with Selenium-Nightwatch.

Since this is my first time using this test suite, I plan to learn about it as I write. If you have any comments, please let me know if I am using it incorrectly

I have checked out the documentation at the Github page of the Selenium-Nightwatch project and installed the package according to following instructions from the author. This is where my problems begun, since author stressed that it is requited to run meteor with administrator privileges. This completely messed up build of all my packages and from this moment on I had to run all my meteor projects with administrator privileges and I shrieked in horror. After several hours of trial and error I messed up my whole Meteor installation and had to reinstall Node.js, Meteor and all Meteorite packages (I deleted them from ~/.meteorite directory). Following is a bash script which will safely bring selenium-nightwatch into your project, without requiring to run meteor as administrator (yet nightwatch still has to be run with administrator privileges). Also, in my approach I install night watch globally, sine the provided scripts try to install night watch into project directory, what many times failed and also required admin privileges to run meteor.

// in terminal
$ cd toYourProjectRoot
$ mrt install selenium-nightwatch
$ sudo -H npm install -g nightwatch
$ ln -s packages/selenium-nightwatch/launch_nightwatch_from_app_root.sh run_velocity_from_app_root.sh
$ ln -s packages/selenium-nightwatch/launch_nightwatch_from_app_root.sh run_root_nightwatch.sh

Now we need to modify the launcher scripts, so that they do not call the local night watch instance, but the global one (I have commented out original functionality and added mine):

# file: packages/selenium-nightwatch/launch_nightwatch_from_app_root.sh
#!/bin//bash
#echo "installing nightwatch in .meteor/local/build"
#  cd .meteor/local/build
#  sudo npm install nightwatch@0.5.3
#  cd ../../../

echo "running nightwatch from app root"
#   sudo .meteor/local/build/node_modules/nightwatch/bin/nightwatch -c packages/selenium-nightwatch/nightwatch_from_app_root.json $1 $2
sudo nightwatch -c packages/selenium-nightwatch/nightwatch_from_app_root.json $1 $2

Also we modify following file

# file: packages/selenium-nightwatch/launch_nightwatch_from_velocity.sh
#!/bin//bash
echo "installing nightwatch in .meteor/local/build"
#  cd .meteor/local/build
#  sudo npm install nightwatch@0.5.3
#  cd ../../../

echo "velocity is launching nightwatch"
#   sudo .meteor/local/build/node_modules/nightwatch/bin/nightwatch -c packages/selenium-nightwatch/nightwatch_from_velocity.json $1 $2
sudo nightwatch -c packages/selenium-nightwatch/nightwatch_from_velocity.json $1 $2

Now, you are ready to run your acceptance tests. Running tests with for display in velocity call simply ./run_velocity_nightwatch.sh in your application root. To run tests without velocity run ./run_root_nightwatch.sh -t path/to/testfile.js. In this tutorial we will be running the test only in terminal, since velocity was crashing our app when parsing selenium report.

WARNING! Up to date I was not able to run selenium-nightwatch with Velocity due to the possible bug in parsing the selenium report files. I will update this tutorial once I will be sure that it works.

We are ready to write our first test! The documentation makes it look dead simple, so let’s get right to it. First, I checked in smart.json file how velocity identifies test suites. In case of selenium-nightwatch package, this information resides in nightwatch_from_velocity.json file and tells us following "src_folders" : ["tests/nightwatch"]. So, we create a directory in /tests/nightwatch and try to code our first acceptance test. In this test, we:

  1. Login as admin in the introduction (home) page
  2. We navigate to “/createTutorial” page
  3. We create a new tutorial
  4. We check that this tutorial was created in “tutorials” page

Later we extend this test, but for now it will suffice. Following is our very first selenium-nightwatch test.

// file: tests/nightwatch/admin-test.js
module.exports = {
    "Hello World" : function (client) {
        client
            .url("http://127.0.0.1:3000")
            .waitForElementVisible("body", 1000)
            .assert.title("Hello World")
            .end();
    }
};

We run this test in terminal with ./run_root_nightwatch.sh -t tests/nightwatch/admin-test.js and obtain following result:

Terminal

Since in our template we did not set the page title our test fails. Great, we got it working! Let’s try to write something more meaningful related to our test!

To be continued …

Chapter 6: Summary

In this tutorial we have covered quite a lot of grounds in developing Meteor applications following the BDD with Jasmine and Velocity. We have been using two testing packages, jasmine-unit for unit testing and jasmine for integration testing. Jasmine-unit is incredibly fast and reliable package and works very well with velocity. Yet it uses old jasmine syntax for writing tests. Jasmine on the other hand, uses the new 2.0 syntax, yet it is very unreliable with Velocity since the mirror server where tests are executed keep crashing quite often and to obtain test result we often have to reload our application.

Since this is a very first version of this tutorial, I really would welcome any comments and suggestions that would help to make this tutorial better, possibly drawing even more examples. Since I am actively developing a medium scale application for students of my university, I will be updating this tutorial with more examples.

Advertisements
Video

How to create low-poly hair for Unity 3D using Blender 3D and Hair Factory (tutorial)

In this tutorial I want to show you how to create low poly hair for Unity 3D using Hair Factory. Below you’ll find a link to the Hair Factory project as well as the code for the Transparent, Double Sided Unity Shader.

Shader "Transparent/Double-Sided Vertex Lit" {
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
    }

    SubShader {
        Tags {"RenderType"="Transparent" "Queue"="Transparent"}
        // Render into depth buffer only
        Pass {
            ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha
            ColorMask RGB
            Cull off
            Material {
                Diffuse [_Color]
                Ambient [_Color]
            }
            Lighting On
            SetTexture [_MainTex] {
                Combine texture * primary DOUBLE, texture * primary
            }
        } 
    }
}

IMPORTANT: If you’ll find ant improvement in the shader or any faults in the video, please let me know.