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

47,191

Solution 1

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');

  });

};

Solution 2

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/

Share:
47,191

Related videos on Youtube

hal
Author by

hal

Updated on January 23, 2020

Comments

  • hal
    hal over 4 years

    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.

    • Louis
      Louis over 9 years
      If your snippets are meant to be modules it would be helpful to show what it is you mean to export from your modules, because it matters to the kind of answers you are going to get.
    • hal
      hal over 9 years
      I updated the example with more detail. The example files are modules in a node app.
  • redOctober13
    redOctober13 almost 5 years
    This was helpful, because I've been struggling with whether I'm writing tests correctly by just seeing if certain functions were called when my component is initialized, and wondering if that's actually helpful. But from what you're saying, it's actually better to test whether the result of calling that function happens, since simply verifying something with a certain name was called doesn't actually increase confidence that things are working as they should.
  • RayLoveless
    RayLoveless over 4 years
    This would be a great answer if it included a spy example also.
  • Muhammed Moussa
    Muhammed Moussa about 3 years
    toHaveBeenCalled is a jest method so it invalid in mocha