11: Testing

Now we’ve created a few features for our application, let’s add a test to ensure that we don’t regress and that it works the way we expect.

We’ll write a test that executes one of our Methods and verifies that it works correctly.

11.1: Install Dependencies

We’ll add a test driver for the Mocha JavaScript test framework, along with a test assertion library:

meteor add meteortesting:mocha
meteor npm install --save-dev chai

We can now run our app in “test mode” by running meteor test and specifying a test driver package (you’ll need to stop the regular app from running or specify an alternate port with –port XYZ):

TEST_WATCH=1 meteor test --driver-package meteortesting:mocha

It should output something like this:

simple-todos-react
  ✓ package.json has correct name
  ✓ server is not client

2 passing (10ms)

From where are these two tests coming? Every new Meteor application includes a tests/main.js module containing several example tests using the describe, it, and assert style popularized by testing frameworks like Mocha.

The community maintains Meteor Mocha integration. You can read more here.

When you run with these options, you can also see the results of the tests in the app URL in your browser:

11.2: Scaffold Test

However, if you prefer to split your tests across multiple modules, you can do that. Add a new test module called imports/api/tasksMethods.tests.js.

imports/api/tasksMethods.tests.js

import { Meteor } from 'meteor/meteor';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      it('can delete owned task', () => {});
    });
  });
}

And import it on tests/main.js like import '/imports/api/tasksMethods.tests.js'; and delete everything else from this file as we don’t need these tests:

tests/main.js

import '/imports/api/tasksMethods.tests.js';

11.3: Prepare Database

You need to ensure the database is in the state we expect before beginning any test. You can use Mocha’s beforeEach construct to do that easily:

imports/api/tasksMethods.tests.js

import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { TasksCollection } from '/imports/db/TasksCollection';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      const userId = Random.id();
      let taskId;

      beforeEach(() => {
        TasksCollection.remove({});
        taskId = TasksCollection.insert({
          text: 'Test Task',
          createdAt: new Date(),
          userId,
        });
      });
    });
  });
}

Here you create a single task associated with a random userId that’ll be different for each test run.

11.4: Test Task Removal

Now you can write the test to call the tasks.remove method as that user and verify the task got deleted. As you are going to test a method and we want to mock the authenticated user, you can install this utility package to make your life easier:

meteor add quave:testing

imports/api/tasksMethods.tests.js

import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { mockMethodCall } from 'meteor/quave:testing';
import { assert } from 'chai';
import { TasksCollection } from '/imports/db/TasksCollection';
import '/imports/api/tasksMethods';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      const userId = Random.id();
      let taskId;

      beforeEach(() => {
        TasksCollection.remove({});
        taskId = TasksCollection.insert({
          text: 'Test Task',
          createdAt: new Date(),
          userId,
        });
      });

      it('can delete owned task', () => {
        mockMethodCall('tasks.remove', taskId, { context: { userId } });

        assert.equal(TasksCollection.find().count(), 0);
      });
    });
  });
}

Remember to import assert from chai (import { assert } from 'chai';)

11.5: More tests

You can add as many tests as you want. Below you can find a few other tests that can be helpful for you to have more ideas of what to test and how:

imports/api/tasksMethods.tests.js

import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { mockMethodCall } from 'meteor/quave:testing';
import { assert } from 'chai';
import { TasksCollection } from '/imports/db/TasksCollection';
import '/imports/api/tasksMethods';

if (Meteor.isServer) {
  describe('Tasks', () => {
    describe('methods', () => {
      const userId = Random.id();
      let taskId;

      beforeEach(() => {
        TasksCollection.remove({});
        taskId = TasksCollection.insert({
          text: 'Test Task',
          createdAt: new Date(),
          userId,
        });
      });

      it('can delete owned task', () => {
        mockMethodCall('tasks.remove', taskId, { context: { userId } });

        assert.equal(TasksCollection.find().count(), 0);
      });

      it(`can't delete task without an user authenticated`, () => {
        const fn = () => mockMethodCall('tasks.remove', taskId);
        assert.throw(fn, /Not authorized/);
        assert.equal(TasksCollection.find().count(), 1);
      });

      it(`can't delete task from another owner`, () => {
        const fn = () =>
          mockMethodCall('tasks.remove', taskId, {
            context: { userId: 'somebody-else-id' },
          });
        assert.throw(fn, /Access denied/);
        assert.equal(TasksCollection.find().count(), 1);
      });

      it('can change the status of a task', () => {
        const originalTask = TasksCollection.findOne(taskId);
        mockMethodCall('tasks.setIsChecked', taskId, !originalTask.isChecked, {
          context: { userId },
        });

        const updatedTask = TasksCollection.findOne(taskId);
        assert.notEqual(updatedTask.isChecked, originalTask.isChecked);
      });

      it('can insert new tasks', () => {
        const text = 'New Task';
        mockMethodCall('tasks.insert', text, {
          context: { userId },
        });

        const tasks = TasksCollection.find({}).fetch();
        assert.equal(tasks.length, 2);
        assert.isTrue(tasks.some(task => task.text === text));
      });
    });
  });
}

If you rerun the test command or left it running in watch mode before, you should see the following output:

Tasks
  methods
    ✓ can delete owned task
    ✓ can't delete task without an user authenticated
    ✓ can't delete task from another owner
    ✓ can change the status of a task
    ✓ can insert new tasks

5 passing (70ms)

To make it easier to type the test command, you may want to add a shorthand to the scripts section of your package.json file.

New Meteor apps come with a few preconfigured npm scripts, which you are welcome to use or modify.

The standard meteor npm test command runs the following command:

meteor test --once --driver-package meteortesting:mocha

This command is suitable for running in a Continuous Integration (CI) environment such as Travis CI or CircleCI since it runs only your server-side tests and then exits with 0 if all the tests passed.

If you would like to run your tests while developing your application (and re-run them whenever the development server restarts), consider using meteor npm run test-app, which is equivalent to:

TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha

This is almost the same as the last command, except that it loads your application code as usual (due to --full-app), allowing you to interact with your app in the browser while running client and server tests.

There’s a lot more you can do with Meteor tests! You can read more about it in the Meteor Guide article on testing.

Review: you can check how your code should be in the end of this step here.

In the next step, we will deploy your app to Galaxy, the best hosting for Meteor apps, developed by the same team behind Meteor.

Edit on GitHub
// search box