Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking/stubbing objects that are only defined in a closure

First of all, for testing my library, I'm using Mocha and Chai, but I'm probably going to need Sinon too sometime.

This is the library:

import Service from 'service'; // a third-party module out of my control

const service = Service(...);

class MyLib {
    ... uses `service` in a bunch of different ways ...
    ... service.put(foo) ...
    ... service.get(bar) ...
}

export default MyLib;

This is basically the test file:

import MyLib from '../my-lib.js';

describe('MyLib', () => {
    describe('a method that uses the service', () => {
        ...

The service object makes some calls to remote servers, which I can't really do in the tests. Therefore, I'm thinking I should stub the service's methods or mock the entire service object. However, since the object is constant and only reachable through the MyLib closure, I don't know how.

Ideally I don't wish to change the API of MyLib to e.g. inject the service object in the constructor.

I use Babel 6 with the es2015 preset, if it matters.

How should I approach this?

like image 313
damd Avatar asked Feb 13 '16 20:02

damd


2 Answers

There are a few ways to do it.

The simplest way without extra libraries

Save service as a class property and call it from there:

import Service from 'service'; 

const service = Service(...);

class MyLib {
    constructor() {
       this.service = service;
    }
    ... now you should call service in a bit different way
    ... this.service.put(foo) ...
    ... this.service.get(bar) ...
}

export default MyLib;

Then you can rewrite service instance in your tests:

it('should call my mock', () => {
   const lib = new MyLib();
   lib.service = mockedService; // you need to setup this mock, with Sinon, for example
   lib.doSomething();
   assert.ok(mockedService.put.calledOnce); // works
});

Mock require() function

There are some libraries that allow you to override results of require() function. My favourite one is proxyquire. You can use it and your module will get mockedSerice instead of real:

import proxyquire from 'proxyquire';

it('should call my mock', () => {
   const MyLib = proxyquire('./my-lib', {
     // pass here the map of mocked imports
     service: mockedService
   })
   const lib = new MyLib();
   lib.doSomething();
   assert.ok(mockedService.put.calledOnce); // works
});

Use rewire to get access into module closure

Rewire is a special library that instruments module code so then you can change any local variable there

import rewire from 'rewire';

it('should call my mock', () => {
   const MyLib = rewire('./my-lib')
   const lib = new MyLib();

   // __set__ is a special secret method added by rewire
   MyLib.__set__('service', mockedService);

   lib.doSomething();
   assert.ok(mockedService.put.calledOnce); // works
});

Also, there is a babel-plugin-rewire for better integration with your tools.

All methods above are nice you may pick that seems better for your issue.

like image 62
just-boris Avatar answered Oct 24 '22 14:10

just-boris


I was dealing with the same thing recently. You can take advantage of https://nodejs.org/api/modules.html#modules_caching In the test you are writing, require/import the service the same way you do in the file you are testing. Then use Sinon to stub the methods you are using within the file to test.

sinon.stub(Service, put).returns()

When the file requires the service, it will use the modified module.

I haven't tested your exact case, where you are creating instance of the service and only after that you work with it, but a bit of playing with it should help you achieve what you want, and it is all without any external libraries, only simple sinon stub.

like image 32
Jakub Folejtar Avatar answered Oct 24 '22 14:10

Jakub Folejtar