Guide to Unit Testing with Meteor

569 views
Skip to first unread message

Sam Hatoum

unread,
Apr 1, 2013, 8:18:42 PM4/1/13
to meteo...@googlegroups.com
Hello 

I've created a guide to doing unit testing using Meteor. You can see it here:


Cheers

Sam

Kevin Harvey

unread,
Apr 18, 2013, 10:28:34 AM4/18/13
to meteo...@googlegroups.com
Hi Sam, thanks for this post! I'm using your testing framework in a new Meteor project I'm developing. How can I test a form submission? Here's my code:


  Settings = new Meteor.Collection("settings");

  Template.settings.events({
    'change #ga_id': function () {
      var settings = Settings.findOne({ domain : window.location.host });
      Settings.update(settings._id, {ga_id: document.getElementById("ga_id").value,
           domain: window.location.host});
    }
  });

.. and in the template itself:

  <template name="settings">

    <h1>Settings</h1>

    <input id="ga_id" type="text" value="{{ga_id}}" />

  </template>

Abigail Watson

unread,
Apr 18, 2013, 11:07:11 AM4/18/13
to meteo...@googlegroups.com
Great writeup, Sam!  Thank you so much for this.  Looking forward to giving it a whirl, and seeing how it works.  :)


On Monday, April 1, 2013 8:18:42 PM UTC-4, Sam Hatoum wrote:

Sam Hatoum

unread,
Apr 18, 2013, 4:51:06 PM4/18/13
to meteo...@googlegroups.com
@Kevin

I would use a combination of unit tests and end-to-end tests. 

The unit test would isolate the 'change #ga_id' event code and test just that. Here's the gist of it - untested code.. ironically (-:

// Stub the browser objects. It's probably worth adding these to a browser stub, I'll get onto that soon
var window = { location: { host: ''} };
var document = { getElementById: function() {} };
 
// Stub your settings template
Template.stub('settings');
 
describe("Change setting", function() {
 
it("updates qa_id setting when the user changes the value", function() {
 
// SETUP all your dependencies and expected values
window.location.host = 'bar.foo.com';
spyOn(Settings, 'findOne').andReturn({ _id: 5 });
spyOn(Settings, 'update');
spyOn(document, 'getElementById').andReturn( { value: 'blah' } );
 
// EXECUTE the single pathway through your code you're testing (in this case, you only have one. If you had more, you'd have more it("...") tests)
Template.settings.fireEvent('change #ga_id');
 
// VERIFY the values made it through the spies correctly
expect(Settings.findOne).toHaveBeenCalledWith( { domain : 'bar.foo.com' } );
expect(document.findElementById).toHaveBeenCalledWith( 'ga_id' );
expect(Settings.update).toHaveBeenCalledWith( 5, { ga_id : 'blah', domain : 'bar.foo.com' } );
 
});
 
});

Then to make sure the template + your code + meteor + mongo are delivering the value to the user, you can do an end-to-end that looks like this:


it("Changing the settings persists when the user comes back to the site", function (done) {
    openApp().
        then(openSettings).
        then(verifyTheGaSettingIs('old setting')).
        then(changeGaTo('new setting')).
        then(openApp).
        then(openSettings).
        then(verifyTheGaSettingIs('new setting')).
        then(finish(done), error);
});

See this blog post for meteor end-to-end testing

Sam







--
You received this message because you are subscribed to the Google Groups "meteor-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to meteor-talk...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Sam Hatoum

unread,
Apr 18, 2013, 4:52:13 PM4/18/13
to meteo...@googlegroups.com
Thanks Abigail and you're welcome.


On 18 April 2013 08:07, Abigail Watson <awats...@gmail.com> wrote:
--

Mário Melo

unread,
Apr 19, 2013, 12:58:18 PM4/19/13
to meteo...@googlegroups.com, s...@hatoum.net
Amazing work Sam!

I'm a real fan of Cucumber. Althought Cucumberjs isn't as good as the ruby one, it stills an awesome way to cover the gap between customer expectations and developer understanding.

Also, it's a nice way to create dynamic testable documentation. Do you think it'd be complicated to use it for atdd/end-to-end tests?

Sam Hatoum

unread,
Apr 19, 2013, 5:34:20 PM4/19/13
to Mário Melo, meteo...@googlegroups.com
Thanks Mário

I considered cucumberjs at the start, but opted for Jasmine BDD for three reasons. 1) it's got decent mocking spies for Unit testing, 2) it's also usable in a BDD fashion for end-to-end and 3) it's one of the two super popular libraries (Mocha being the other) that front-end devs are familiar with. 

I don't see any reason why you can't use it though, the benefit of which is the standard feature definition language popular amongst the savvy business people. I'm personally quite liking then(...) chaining as it reads quite naturally.


Kevin Harvey

unread,
May 14, 2013, 8:15:21 PM5/14/13
to meteo...@googlegroups.com
Sam, thanks so much for the blog post! I'm humming along with several tests in my project.  If I may continue to lean on your expertise... :)

I'm writing a test for another event that uses a bit of jQuery, and I'm finding myself trying to mock the function that I'm trying to use ($.closest()). Is that the best way of going about it, or (even better) is there some way I can make jQuery available to the test environment? 

My function:

'click .set-role-toggle a': function (event) {
  var clicked_anchor = event.toElement;
  var user_tr = $( clicked_anchor ).closest('tr');
  if ( clicked_anchor.getAttribute("class").indexOf("set-has-role") != -1 ) {
    Meteor.call( 'removeUserFromRole', user_tr.attr( "data-user" ), clicked_anchor.getAttribute('data-role'));
  } else {
    Meteor.call( 'addUserToRole', user_tr.attr( "data-user" ), clicked_anchor.getAttribute('data-role'));
  }
}

I added the following to stubs.js to try to mock jQuery:

var $ = function () {};
$.prototype = {
  closest: emptyFunction
};

My test (that fails with closest() method does not exist):

describe( "Template.settings [click .set-role-toggle a]", function () {
  it("adds a user to a role", function () {
    var tr = document.createElement("tr");
        tr.setAttribute("data-user", "12345");
    var td = document.createElement("td");
td.setAttribute("class", "set-role-toggle");
    var a = document.createElement("a");
        a.setAttribute("data-role", "staff");

    td.appendChild(a);
    tr.appendChild(td);
    spyOn(Roles, 'addUsersToRoles'); // using meteor-roles
        spyOn($, 'closest').andReturn(tr);
    Template.settings.fireEvent("click .set-role-toggle a", {toElement: a});
    expect(Roles.addUsersToRoles).toHaveBeenCalledWith( '12345' , 'staff' );
  });

});

Kevin

On Monday, April 1, 2013 8:18:42 PM UTC-4, Sam Hatoum wrote:

Sam Hatoum

unread,
May 14, 2013, 9:31:17 PM5/14/13
to meteo...@googlegroups.com
try this:

var emptyFunction = function () {
};

var JQuery = emptyFunction;
JQuery.prototype = {
    ready: emptyFunction,
    closest: emptyFunction
};
jQuery = new JQuery();

$ = function () {
    return jQuery;
};


--

Sam Hatoum

unread,
May 14, 2013, 9:35:32 PM5/14/13
to meteo...@googlegroups.com
You can easily include jQuery by putting it somewhere like /test/lib/jquery.js and then referencing it in karma.conf.js in the files array, but I would recommend against it as your unit tests will become integration tests (more than one unit). Using the above approach, it takes 2 seconds to remove the dependency on jQuery and make sure you're focussed on testing your units. I find this keeps me in check by ensuring all my methods are modular.
Reply all
Reply to author
Forward
0 new messages