Testing methods which use this.userId

Is there a good way to test methods which use this.userId? For now I added a mock for Meteor.userId() (returns a random id) and then I replaced this.userId with Meteor.userId() in the method itself. It seems like a bit of a hack to change how I would write the method just to facilitate testing.

3 Likes

Which testing framework are you using and what kind of test is it (integration / unit, client / server)?

May just need to be added as a bug to the repo for whichever framework you’re using.

Unit testing methods on the server with tinytest.

Oh, I assumed you were using velocity. Since Tinytest runs in-context (integration) I would have figured this.userId() would have worked.

Is the problem that there is no user set? In that case, this.setUserId() should do the trick.

Maybe you could do a small repro?

I’m sure this.userId would work great, the trick is that I don’t want to create a new user and sign in for each test. I mean I guess I could but that seems silly.

I thought of using this.setUserId() but it can only be called in a method which is initiated by the client. I assume there’s a way to circumvent this, but it may be more trouble than it’s worth if the solution if more than a couple of lines of code.

See https://github.com/meteor-velocity/meteor-stubs/blob/master/index.js#L237-L273 for how to unit test your methods.

Sanjo would you provide us an example please? sorry I don’t get it :’( , how we should apply?

Sorry, I forgot one aspect. How it’s done in the linked code only works when the Meteor.methods calls are mocked. That’s the case in sanjo:jasmine Server Unit mode.

What you can do is extracting the business logic out of the Meteor methods and unit tests the extracted functions.

For the Meteor methods I think you can only write integration tests. So you need to really login.

To clarify this a bit for beginners who may be reading, tiny test is an integration testing framework. This means that your tests will run in the meteor context just like when your main app runs.

Because the meteor method in David’s case uses this.userId and expects the user to be logged in, the options for testing that method directly are to either:

  1. Log in before testing the method,
  2. Mock this.userId so that it returns something, or
  3. Refactor the guts of the method into a separate function that takes the userId as a param and test that instead.

In this case, doing #2 is challenging because you’d have to modify the this context of the method before its called. Tinytest does not seem to provide hooks to do that.

As David mentioned, changing the method code to use Meteor.userId instead allows direct overriding of the function (JavaScript is a dynamic language). This works but may not be ideal since it requires changes to existing code just to test it which is generally something to avoid.

Jonas showed an alternate way of testing the method using sanjo:jasmine which provides unit testing functionality (in addition to integration testing). With unit tests, the code is isolated and does not run in the meteor context. This normally means that more care needs to be taken to mock dependencies. But in this case sanjo:jasmine provides a handy way to directly set the context of a meteor method call in order to unit test it.

For those interested in learning more about this kind of thing, the http://velocity.meteor.com page has more info about the different types of testing options available.

4 Likes

Also see here for some unit testing examples with UserId usage

https://github.com/xolvio/Letterpress/blob/master/tests/jasmine/server/unit/chaptersSpec.js#L3

Hello Sanjo, could you please go into little bit more detail on how it would be done for a server side integration test?

Either how do I spyOn the this.userId method, or how to I extend the stubs to return a specific id when this.userId will be called?

I’m running into the same issue, using tinytest too.

userId = Meteor.userId()
 - exception - message Object #<Object> has no method 'userId'

so the method itself doesn’t exist - not that it hasn’t been set to something valid.
is the suggestion to create a stub for this method for testing?

related: what velocity framework is most stable?
every time i try to use it i run into weird bugs and give up again. I don’t need all the esoteric features just something at tinytest level but for an app.

I’ve been doing something like this with tinytest and sinon:

var USER_ID = Random.id();

var reset = function(test) {
  test.stub(Meteor, 'userId', function() {
    return USER_ID;
  });
  //do more reset stuff here
};

Tinytest.add('something', sinon.test(function(test) {
  reset(this);
  // do more test stuff here
}));

yeah, that would work. Sinon Spying looks interesting.
I just made a wrapper around Meteor.userId() . At least it’s simple enough that i can understand it :smile: and hopefully it won’t catchall other errors…

# stub function for testing
dclib.userId = () ->
  try
    userId = Meteor.userId()
  catch
    userId = "TESTUSER"
  return userId
1 Like

Hi guys,

I just ran into a scenario where I want to stub this.userId() used within a method.run().

Has anyone a quick solution for this??

For simplicity let’s say this is my method.js:

import { ValidatedMethod } from 'meteor/mdg:validated-method'

export const MembersUpdateMethod = new ValidatedMethod({
  name: 'Members.methods.update',
  validate(options) {
    console.log(this.userId)  // HOW do I stub this.userId in my unit-tests?
    // do stuff with this.userId
  },

  run(options) {
    console.log(this.userId)  // HOW do I stub this.userId in my unit-tests?
    // do stuff with this.userId
  },
})

and this is my method.test.js:

import { sinon } from 'meteor/practicalmeteor:sinon';

it('MembersUpdateMethod.run works as expected', () => {
  // QUESTION: HOW do I stub this.userId used within ``MembersUpdateMethod.run``
  // ... this does NOT work
  sinon.stub(this, 'userId', () => 'stubbedUserId')  // Error: Attempted to wrap undefined property userId as function
  MembersUpdateMethod.run({ param: 'param' }})  // I'd love to have this.userId return "stubbedUserId"
})

You can supply the context of your function execution:

it('MembersUpdateMethod.run works as expected', () => {
  MembersUpdateMethod.run.call({userId: 'stubbedUserId'}, { param: 'param' }})  // I'd love to have this.userId return "stubbedUserId"
})

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call

This one really works even with validate-methods validation!

2 Likes

I needed to write a test to show a publish function is returning the documents the user should see. Publish functions use this.userId so I had to find a way to create a user and set their _id as the current user. Here’s how I put it together. I hope this will save somebody the time it took me to figure out all the parts!

PublicationCollector is the secret sauce, it lets you pass in the current user to the publish function.

The code below creates a Journal object which is published. It creates a user in the database, and passes their _id in to the PublicationCollector so that the publish function can see it as expected. The publish function is whatever you want, it doesn’t have to be modified for the test. In my case I find the current user and check their role before deciding what journals to return.

Note that the npm modules faker, sinon had to be imported like:

meteor npm install --save faker

And you need to import your collection definition (schemas in my case) and your publish function.

in journals.test.js:

import { Meteor } from 'meteor/meteor';
import { Factory } from 'meteor/dburles:factory';
import { PublicationCollector } from 'meteor/johanbrook:publication-collector';
import { resetDatabase } from 'meteor/xolvio:cleaner';
import faker from 'faker';
import { Random } from 'meteor/random';
import { chai, assert } from 'meteor/practicalmeteor:chai';
import sinon from 'sinon';
import { Journals } from '../../schemas/schemas';
import './publish';

Factory.define('journal', Journals, {
	'title': () => faker.lorem.words(),
	'description': () => faker.lorem.text(),
	'createdAt': () => new Date(),
});

Factory.define('user', Meteor.users, {
	'username': 'Dalai lama',
});

if (Meteor.isServer) {
	describe('Journals', () => {
		beforeEach(function () {
			resetDatabase();

			const currentUser = Factory.create('user');
			sinon.stub(Meteor, 'user');
			Meteor.user.returns(currentUser);

			Factory.create('journal');
		});

		afterEach(() => {
			Meteor.user.restore();
			resetDatabase();
		});

		describe('publish', () => {
			it('can view own journal', (done) => {
				const collector = new PublicationCollector({ 'userId': Meteor.user()._id });
				collector.collect(
					'journals',
					(collections) => {
						assert.equal(collections.journals.length, 1);
						done();
					},
				);
			});
		});
	});
}
3 Likes