Meteor 1.3 testing - with "meteor/meteor:x" package imports


#1

Testing code that’s depending on Meteor modules can be quite hard. Even the blog mentioned at unit-testing-with-meteor-1-3 is depending on the Meteor global. And depending on globals is something we prefer not to do. That’s why we are working with import statements, isn’t it?

So this is why I’m sharing my solution here. No more use of globals, import it all, but keep it testable.

In this example we depend on two modules, meteor/meteor and kadira:flowrouter. It is an example, so forgive me that the code doesn’t really make sense.

Let’s asume we want to have these methods tested:

// posts.js

import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flowrouter';

export const post = (state, action) => {
  switch (action.type) {
    case 'SAVE_POST':
      return Object.assign({}, state, {
        userId: Meteor.userId(),
      });
    default:
      return state;
  }
};

export const savePost = (data) => {
  const newPost = post(data, {
    type: 'SAVE_POST',
  });

  FlowRouter.go('/home');
}

When we try to test this by running mocha, we will find two problems here. Both meteor/meteor and meteor/kadira:flowrouter resolve to the error: Cannot find module meteor/....

It appears that proxyquire is able to solve this issues, by proxying the import statements. And with sinon we can stub the relevant parts out. Eventually, it is for the best, because those part aren’t us to be tested anyway.

So, go install those two packages:

npm install --save-dev proxyquire
npm install --save-dev sinon

The required stubs for the test will look something like:

// tests/post.js - first part

/* eslint-env mocha */
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';

const Meteor = {
  userId: sinon.stub(),
};

const FlowRouter = {
  go: sinon.spy(),
};

// import { post, savePost } from '../post.js';
const {
  post,
  savePost
} = proxyquire('../post.js', {
  'meteor/meteor': { Meteor, '@noCallThru': true },
  'meteor/kadira:flowrouter': { FlowRouter, '@noCallThru': true },
});

Instead of declaring an import { post, savePost } from '../post.js'; we make a call to proxyquire and use object destructing to get the imported functions in our local scope. The object we pass as second argument to proxyquire, specifies the stubs to be used.

You’ll see that the import to meteor/meteor is being replaced by our provided Meteor object. The '@noCallThru': true is required to make sure that the original package is not being included in any way. Without providing this, proxyquire will wrap the object instead of replacing it. As we cannot wrap something that doesn’t exists, we need to replace it completely.

Making tests like the one below run just fine:

// tests/post.js - second part

describe('posts', () => {
  before(() => {
    Meteor.userId.returns('user_1');
  });

  it('now stubs Meteor packages', () => {
    const result = post(
      { title: 'foo', body: 'bar' },
      { type: 'SAVE_POST' }
    );

    expect(result).to.have.property('userId', 'user_1');
  });

  it('can spy on Meteor packages', () => {
    const result = savePost(
      { title: 'foo', body: 'bar' },
      { type: 'SAVE_POST' }
    );

    expect(FlowRouter.go.calledOnce).to.be.true
    expect(FlowRouter.go.calledWith('/home')).to.be.true;
  });
});

Now try running these tests like me with wallaby, or with mocha and expect to see some nice green tests:

$ mocha **/tests/*.js --compilers js:babel-core/register


  posts
    √ now stubs Meteor packages
    √ can spy on Meteor packages


  2 passing (44ms)

I hope you’ll find this useful. If you have any tips yourself to improve this further, please let us know :slight_smile:


One huge benefit that this gives me, instead of using the new meteor test tools, is that I can use wallaby for my tests. This wouldn’t be possible when I’m depending on the global meteor variables. Actually, making wallaby work, was the main reason I was looking for a way to stub out the meteor framework.


Meteor test very long build times
How can I speed up unit tests with 1.3 and Mocha?
First Impressions: Meteor 1.3 Official Testing Support
Improving coding productivity
[SOLVED] Mocha is unhappy about absolute paths and meteor imports
Improving coding productivity
#2

Beautiful! Good work in writing this up. Meteor’s “magic” imports were definitely the elephant in the room in regards to my 1.3 unit testing post. This will definitely be very useful.


#3

Awesome work, thanks for sharing.

@sanjo took a different approach to use Wallaby that you can see here. He basically loads the Meteor context directly.

It’s cool as it gives you the flexibility to chose between using proxy require, or actually letting the import do it’s magic.


#4

Love it! Very concise solution!
Mantra also uses a very smart way of injecting dependencies and mocking Meteor packages concisely, but your solution is more intuitive to work with, especially for apps that are not laid out using mantra’s architecture.

@sanjo’s work on configuring Wallaby is also excellent work definitely, but I feel it’s a little intimidating to digest.


#5

@voiceup Totally understand that it’s intimidating, I still have trouble reading it!

With regards to proxyrequire, another (untested) option might be to use testdouble.js, which has a much terser syntax than sinon + proxyrequire.

Just adding some food for thought here
:burrito: :thinking:


#6

Very interesting library. Thanks for sharing!

I was trying to test it out, but couldn’t get it working correctly. I think the reason is It doesn’t work with external commonjs modules. But I like how the author is trying to tackle some of the challenges and inconveniences with sinonjs.


#7

@sam testdouble is looking very nice indeed. But it’s exactly as @voiceup is saying. “It (td.replace) doesn’t work with external commonjs modules”.

*edit; let’s hope they are willing to reconsider this decision.

*edit 2; this appears not to be true. See my comment below.


#8

Most of the bootstrap code in the wallaby_server.js is from the boot.js script that Meteor uses to to start the app. I have just modified it, so it only loads Meteor packages.

(I still need to update the wallaby_server.js to use the latest version of boot.js)


#9

I found out testdouble can be used after-all by using a require statement instead of an import. I did not realize at first, because in my understanding babel would have compiled import statements to require statements behind the scenes. And although this is true, it appeared that babel also moves the import statements to the top of the file.

Thereby; the import statements are being placed above the td.replace statements; and thereby nothing is being replaced after all.

You can easily verify this by pasting the code below in the babel repl, and see that:

td.replace('meteor/meteor', { Meteor });
import{ post } from '../posts.js';
const { savePost } = require('../post.js');

will be transpiled to:

var _posts = require('../posts.js');
td.replace('meteor/meteor', { Meteor: Meteor });
var _require = require('../post.js');
var savePost = _require.savePost;

So translating the opening post’s example from proxyquire + sinon to testdouble, makes the following test-case:

// tests/post.js - first part

/* eslint-env mocha */
import { expect } from 'chai';
import td from 'testdouble';

const Meteor = td.object(['userId']);
const FlowRouter = td.object(['go']);

td.replace('meteor/meteor', { Meteor });
td.replace('meteor/kadira:flowrouter', { FlowRouter });
const { post, savePost } = require('../post.js');

Note that we use require for the import of ../posts.js to make the replacements work. At first I would have preferred to keep using imports. But on the other hand, now there is a visual indicator of importing files as they are, and importing files with applied replacements.

And the testing itself:

// tests/post.js - second part

describe('posts', () => {
  before(() => {
    td.when(Meteor.userId()).thenReturn('user_1');
  });

  after(() => {
    td.reset();
  });

  it('now stubs Meteor packages', () => {
    const result = post(
      { title: 'foo', body: 'bar' },
      { type: 'SAVE_POST' }
    );

    expect(result).to.have.property('userId', 'user_1');
  });

  it('can spy on Meteor packages', () => {
    const result = savePost(
      { title: 'foo', body: 'bar' },
      { type: 'SAVE_POST' }
    );

    td.verify(FlowRouter.go('/home'));
  });
});

You’ll see that these tests get an after method to undo the rewires after the tests have been run. Do not forget this! Mocha will forgive you, but Wallaby will not, as wallaby reuses the node processes.

As wallaby reuses node processes, and can stop a running test script half way the cycle to start it again, because you for example keep typing, it is advised to add the testdouble.reset in your wallaby.setup function. In this way we are sure that no modules are stubbed out before our testfiles are being reloaded. This is in addition to the after method! It’s not an replacement.

// wallaby.js
setup: () => {
  require('testdouble').reset();
},

Using wallaby with Meteor 1.3 and Meteor packages
Meteor test very long build times
Improving coding productivity
#11

Most excellent work @smeijer!


#12

Merging back in a separate conversation that started here: Meteor test very long build times

If I’m understanding things correctly is there still a bit of an issue here, not with the solutions above but in general: It seems like we’re saying it’s fine for the unit tests or the code being tested to import npm modules and use their functionality in the system under test, but you can’t do the same with the meteor modules, instead you have to stub them out?

That makes good sense where those Meteor modules are for things that shouldn’t be tested as part of the code you are unit testing (routing, sending and receiving data from the server etc); it makes complete sense to stub those modules out.

However, there are some Meteor modules that I do want to be tested as part of my unit tests, I’m happy to accept them as black boxes and I want my unit tests to fail if those modules change in some way after I update them.

One example of that is underscore; I don’t want to have to stub out underscore, and even if I did the code I was testing would then stop working unless I faked underscore’s behaviour in some way. I could get round this by using underscore from npm, but then I would be sending two versions of underscore to the browser (I think, because Meteor uses underscore too?). And really, that’s a bit of a hack as effectively now I’m saying saying “it’s OK to use this if it comes from npm, but not from Meteor” which is then a decision based on implementation rather than the requirements for the test.

Essentially, I guess there are Meteor modules that I want to treat like JavaScript’s built-in functions (I wouldn’t stub out toExponential say).

Does anyone else see this as an issue?


#13

I think your reasoning is fine and there are two solutions available to your problem today:

  • Use the Meteor 1.3 test runner (meteor test)
  • Use the Node.js code from the Meteor boot.js file to load the Meteor packages into your Node.js testing process. I use this technique and have roughly outlined it in my previous post.

This is one of the symptoms of the separate Meteor package system. It is hard to use Meteor packages in a Node.js context. But Sashko and Zoltan have indicated that they are aware of this issue and Meteor might use NPM in the future.


#14

@Sanjo I’ve been playing with your wallaby_server.js file, and after some struggles I got it to work.

One question though, are you aware that you must use Fibers within your tests? And do you know a way around this?

For example when I try to reset the database in a beforeAll hook, I get the most famous:

“before all” hook: Meteor code must always run within a Fiber. Try wrapping callbacks that you pass to non-Meteor libraries with Meteor.bindEnvironment.

Running the code all in a fiber solves the issue, but I don’t like stuffing up my tests with fibers. Do you know of a way so Mocha does this for you?

Fiber(() => {
 //..
}).run();

#15

I haven’t done this for Mocha myself, but here is the related work:


#16

Thanks @Sanjo, JS testing is new to me (.net background) so I’m still getting up to speed on all of this.

Just to double check, do you mean use the 1.3 test runner separately without Wallaby, or is it potentially possible to set Wallaby up to use meteor’s test runner?

Also, would that approach then mean waiting for a Meteor build to see the results of the code changes? That’s what I’m keen to avoid, I’m trying to get to the super fast red-green feedback that I have in nCrunch or when using wallaby on pure JS classes outside Meteor.


#17

If you want to use Wallaby, the Meteor test runner (meteor test) is not an option.

We have what you want working, but it’s not super beginner friendly yet. It lacks some detailed documentation and abstractions to make the code easier to use. Check out what I wrote recently in https://github.com/xolvio/automated-testing-best-practices/issues/27 and also just clone and try the repo. Then try to copy the necessary files and adjust the Wallaby configs to your needs.

If you are willing to pay money, we can also help you with our paid Xolv.io support. I will make the code more easier to use eventually, but currently I have a high priority on a new product, that we develop :wink:


Meteor 1.3 unit testing and meteor module imports
#18

I’m successfully using proxyquire to test my app. Thanks so much @smeijer for writing this up. It’s been a revelation to have instant test feedback.

One caveat I’ve found is that it doesn’t support absolute imports. So something like…

import Foo from `/lib/services/foo`;

…will not work proxyquire. I believe this is because require will only work a relative path so you’ll need to use:

import Foo from `../../services/foo`;

#19

Hey all,

I wrote up a follow-up blog post that summarizes a lot of this work. It describes using Testdouble.js to mock Meteor-style imports.

Check it out and let me know if I missed anything.


#20

Thank you all for your work on this. Have any of you had any luck using testdouble with Wallaby? I tried @sanjo’s wallaby_server, took a bit of reconfig for Mantra, but I never got it to successfully work loading meteor packages.


#21

Thanks for the “credit where credit is due” :relaxed: