Starting with Meteor Unit Testing

Kesha Shah
Fasal Engineering
Published in
4 min readJan 31, 2023

At Fasal, our primary web application is built using Meteor and we have decided to implement unit testing as a company practice.

Currently, there are limited solutions available for Meteor version 2.8.1, which is what we use in our codebase. Most resources that we found were outdated or focused on end-to-end testing instead of unit tests.

Our objective was simple: Write unit tests -> Create pull request -> Run BitBucket pipeline -> Generate test coverage report.

This article outlines the steps we took to get started with unit testing in Meteor.

Determining which code to test

As we already have automated tests for various UI scenarios, we initially decided to focus only on server tests and leave out client-side tests.

Deciding the Tech Stack

Meteor has its own in-built test tooling. We have added a few more integrations with it to enhance our unit testing capabilities.

Test Framework — Mocha

Mocks/Stubs/Spies — Sinon

Assertions — Chai

Coverage Tool — Istanbul with meteor coverage

Setup and Configuration

babel configuration in package.json

  "babel": {
"env": {
"COVERAGE": {
"plugins": [
"istanbul"
]
}
}
}

Additional Dependencies in package.json

"chai": "^4.3.7",
"coffeescript": "^2.7.0",
"sinon": "^15.0.0",
"babel-plugin-istanbul": "^6.1.1",

Additional Meteor Packages in .meteor/versions

dburles:factory@1.1.0
lmieulet:meteor-coverage@4.0.0
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.0.1
velocity:meteor-stubs@1.1.1
xolvio:cleaner@0.4.0

Test script in package.json:

  "scripts": {
"unit-test-coverage": "BABEL_ENV=COVERAGE COVERAGE=1 COVERAGE_VERBOSE=1 COVERAGE_OUT_TEXT=1 COVERAGE_OUT_TEXT_SUMMARY=1 COVERAGE_OUT_JSON=1 COVERAGE_APP_FOLDER=$PWD/ meteor test --port 3001 --driver-package meteortesting:mocha TEST_CLIENT=0 --once"
}

Here we kept TEST_CLIENT=0 (To just enable server-side tests)

Please note with the current version meteor coverage COVERAGE_OUT_TEXT is not supported (as of Jan 2023)

Adding Unit Tests

Below are the basic things we wanted to cover while writing tests:

  • Setup in-memory database for all our DB queries
  • Mock Meteor inbuilt methods i.e Meteor.User(), this.unblock()
  • Mock Dependencies (i.e mock all imports/require modules)
  • Assert responses as well as exceptions

Meteor gives a factory package to create mock data in its in-memory database

import assert from 'assert';
import {Meteor} from 'meteor/meteor';
import {Factory} from 'meteor/dburles:factory';
import sinon from 'sinon';

import {resetDatabase} from 'meteor/xolvio:cleaner';
import {expect as chaiExpect} from 'chai';
import {plot, farm, financeSeason} from '../../server/collection';

With help of meteor/dburles:factory, you can set up Meteor User configuration as well as your other DB collections.

These are our setup and teardown methods:

if (Meteor.isServer) {
let mockUser;
describe('meteor test', () => {
beforeEach(async function () {
resetDatabase();
Factory.define('user', Meteor.users, {
profile: {
username: 'test123',
password: 'password'
},
customer: 'test-customer',
_id: 'userid'
});
mockUser = Factory.build('user');
sinon.stub(Meteor, 'user');
await Meteor.user.returns(mockUser);
});

afterEach(() => {
sinon.restore();
});
  • Here for each Meteor.User() method will return mockUser data.
  • We are doing resetDatabase() so that one test-case setup does not impact other test cases.
  • After each test, it will restore all sinon stubs and spies.

This is our meteor method: getFarmById()

 Meteor.methods({
getFarmById(farmId) {
this.unblock();
return farm.findOne({_id: farmId});
}
});

For the above method this is one of the test cases:

it('getFarmById: farm exist - return farm object', async () => {
const mockFarmName = 'test-farm';
await Factory.define('farm', farm, {
_id: mockFarm,
customer: 'test-customer',
name: mockFarmName
});
const mockFarm = await Factory.build('farm');
const test = sinon.stub(farm, 'findOne');
test.withArgs({_id: mockFarmName}).returns(mockFarm);
const result = await Meteor.server.method_handlers.getFarmById.apply(
{
unblock: sinon.fake()
},
[mockFarmName]
);
chaiExpect(result.name).to.equal(mockFarm);
test.reset();
});
  • To mock this.unblock() we are passing unblock: sinon.fake() so it will simply ignore that line and proceed to the next line.
  • Also for every stub we are doing reset() so that the same stub does not impact other test cases.
  • For each database call we need to first insert an entry in the database via Factory.define() and Factory.build()
  • For assertions, we are using Chai as it gives a much-simplified way to assert all types of responses.
  • We have used Meteor.server.method_handlers to handle try/catch errors but you can also use Meteor.call() to invoke your method. i.e
Meteor.call('getFarmById', mockFarmName, {unblock: sinon.fake()});
  • For any method throwing an exception, this is how we handle it:
 try {
await Meteor.server.method_handlers.createNewPlot.apply({unblock: sinon.fake()}, [mockPlot, true]);
} catch (e) {
chaiExpect(e.message).to.contain('Unable to get location details for the plot. Please try again');
}
  • To Mock internal Meteor calls within the method use sinon stub i.e
Source File:   
const duplication = Meteor.call('checkForDuplicationOfNodes', sensorNodes, plotDetails.farmId, user.customer);

Test File:
const meteorCallStub = sinon.stub(Meteor, 'call');
meteorCallStub
.withArgs('checkForDuplicationOfNodes', mockPlot.sensorNodes, mockPlot.farmId, mockUser.customer)
.returns(duplication);
  • To Mock dependency added in my source file i.e
Source File: 
const utils = require('../utility/utility.js')
const isEntitled = utils.entitlementAndAccessCheck(user._id, user.customer, 'SUPER_ADMIN', 'ENABLE_FARM_MANAGEMENT')

Test File:
const entitlementCheckMock = sinon.stub(utils, 'entitlementAndAccessCheck');
entitlementCheckMock.withArgs('userid', 'test-customer', 'ADD_FARM', 'ENABLE_FARM_MANAGEMENT').returns(true);

Please Note: Here you will not be able to mock ES6 imported function i.e

import {entitlementAndAccessCheck} from '../utility/utility';
entitlementAndAccessCheck(user._id, user.customer, 'SUPER_ADMIN', 'ENABLE_FARM_MANAGEMENT')

We tried multiple ways using proxyquire and other libraries but it did not work and we ended up changing our ES6 imports to require modules

Setup bitbucket pipeline to run and measure test coverage

We have set up a bitbucket pipeline to run tests on every pull request and generate coverage reports. We will cover the details of that in another article.

Conclusion

Finally, when you run the tests, you will get a coverage report generated by Istanbul. This will show you which parts of your code are covered by tests and which are not. It is a good practice to maintain at least 80% test coverage in your code.

That’s it! This is how we started with Meteor Unit Testing in Fasal. We hope this will help others who are starting with Meteor Unit Testing. If you have any questions or suggestions, please let us know in the comments.

--

--