clifdensprit
clifdensprit

Reputation: 4901

How do I test if a function calls a specific method/function?

Is there a way in Mocha to test if a function calls a specific method or external function?

I am using Mocha with Chai, but am open to any other assertion libraries.


Ok, so testing whether a methid is being called is pretty easy using sinon. I'm not sure about testing to see if an external function is being called though. So I updated the examples to represent something a little more "real world". I am working on a node app, so foo.js and bar.js are both modules.

Example:

foo.js

var bar = require('bar');
var xyz = function () {};

var Foo = module.exports = function () {
  this.bar();
  bar();
  xyz();
};
Foo.prototype.bar = function () {};

bar.js

var bar = module.exports = function () {};

fooSpec.js

var chai      = require('chai');
var sinon     = require('sinon');
var sinonChai = require('sinonChai');
var expect    = chai.expect;
var Foo       = require('../lib/foo');

chai.use('sinonChai');

describe('Foo', function () {

  var method;

  beforeEach(function (done) {
    method = sinon.spy(Foo.prototype, 'bar');
    done();
  });
  afterEach(function (done) {
    method.restore();
    done();
  });

  it('should call Foo.prototype.bar() immediately', function () {

    new Foo();
    expect(method).to.have.been.called;
    
  });

  it('should call the module bar immediately', function () {
    // ????????????
  });

  it('should call xyz() immediately', function () {
    // ????????????
  });
});

So as you can see I've figured out how to test for Foo.prototype.bar, but I can't find a way to implement the second and third tests.

Upvotes: 25

Views: 57003

Answers (2)

jpenna
jpenna

Reputation: 9161

This is now outdated. The API is different. I will leave the response because the reasoning is still up to date.


I'm using expect assertion library with Mocha, but Chai might have analogous methods


First

You can test if a function calls a specific method/function using Spies. You did this in you code above.

Second

The problem with the code you are testing is Context. So I will address it in this answer. You can test if an external function is called, but it needs a context, so you might have to change your code.

I'm using bar (module) as example. For xyz (function) go to the Second method. The explanation is the same for both.

1. Export bar inside an object

bar.js

var bar = module.exports = { 
  bar: function () {}; 
}

foo.js

var Foo = module.exports = function () {
  bar.bar();
  ....
};

This way you can spy on it doing:

fooSpec.js

it('should call the module bar immediately', function () {

  //note I'm getting the bar method from the exported object (bar module)
  var bar = expect.spyOn(bar, 'bar'); 

  new Foo();

  expect(bar).toHaveBeenCalled();

2. Set bar module as Foo's prototype method

If you don't want to change bar.js, you may set the required module as a prototype method of Foo. Then you have a context to spy on.

foo.js

var bar = require('./bar');

var Foo = module.exports = function () {
  this.bar();
  this.barModule();
};
Foo.prototype.bar = function () {};
Foo.prototype.barModule = bar; // setting here as barModule

fooSpec.js

it('should call the module bar immediately', function () {
  var barSpy = expect.spyOn(Foo.prototype, 'barModule');

  new Foo();

  expect(barSpy).toHaveBeenCalled();    
});

Explanation

The changes you must do are for changing the context of your variables.

To make it clear:

var bar = require('bar');

var Foo = module.exports = function () {
  this.bar();
  bar();
};
Foo.prototype.bar = function () {};

In this snippet, you are requiring bar and later setting this.bar using Foo.prototype. So, how can you set 2 variables with the same name and reference each other nicely?

The answer is Context and Scope. Your this.bar is referencing the bar variable set in this context (which points to Foo). On the other hand, your bar - note there is no this - is referencing the bar variable set in the function's (module) scope.

So, you may test your Foo.prototype.bar, since it is a module method, has a context and you may spy on it. Buy you can't spy on the required bar because it is scoped (think of it as private).

Good read: http://ryanmorr.com/understanding-scope-and-context-in-javascript/

Upvotes: 3

clifdensprit
clifdensprit

Reputation: 4901

So this question was really two in one.

Firstly, "how to test if a method is being called": I laid out the code for this in the example, but basically, using sinon.js you just wrap the method in a "spy" which allows you to write a test that expects that spy to have been called.

Secondly, "how to test if a private function(one that was not exported as part of the module) has been called":

Basically, you don't. It is possible to export these functions when in a testing environment and not in production, but this seems a little too hacky to me.

I've come to the conclusion that when calling another module you should just break the TDD cycle and not test for this since it's probably going to be a small amount of code and the module will have already been tested on it's own.

If you are calling a private function that is declared within you're module and want to test it you should write a more broad test that tests for the result of this function being called rather than testing whether the function is being called or what is actually happening within the function.

Here's a really simple example:

foo.js

var _ = require('lodash');

var Foo = module.exports = function (config) {

  this.config = _.merge({
      role: 'user',
      x: '123',
      y: '321'
    },
    config);

  this.config.role = validateRole(this.config.role);
};

var validateRole = function (role) {
  var roles = [
    'user', 'editor', 'admin'
  ];

  if (_.contains(roles, role)) {
    return role;
  } else {
    return 'user'
  }
};

fooSpec.js

var chai = require('chai');
var expect = chai.expect;
var Foo = require('../lib/foo');

describe('Foo', function () {

  it('should set role to \'user\' if role is not valid', function () {

    var foo = new Foo({role: 'invalid'});
    expect(foo.config.role).to.equal('user');

  });

};

Upvotes: 5

Related Questions