简体   繁体   中英

Mocking modules in Node.js for unit testing

I want to unit test some functions in a node.js module. I think that mocking a 3rd module would be helpful. In particular to avoid hitting the database

# models/account.coffee
register = (email, password)->
   sha_sum.update(password)
   pw = sha_sum.digest('hex')
   user = 
      email: email
      password: sha_sum.digest('hex')

   users_db.save user, (err, doc)->
      register_callback(err)

account_module = 
   register: register

module.exports = account_module

This is the module that i want to test

# routes/auth.coffee
account = require '../models/account'

exports.auth = 
   post_signup: (req, res)->
      email = req.body.email
      password = req.body.password
      if email and password
          account.register(email, password)
          res.send 200
      else
          res.send 400

I want to be able to test that hitting this url with the correct body in the post calls the account.register function but i don't want the test to hit the database. I may not have implemented the account module yet.

The jasmine spec # specs/auth.test.coffee describe 'signup', ->

   request = require 'request' 
   it 'should signup a user with username and password', (done)->

       spyOn(account, 'register') # this does not work, account.register still called
       url = root + '/signup'
       headers =
           "Content-Type": "application/json" 
       data = 
           email: 'user@email.com'
           password: 'pw'
       body = JSON.stringify(data)
       request {url: url, method: 'POST',json: data, headers: headers }, (err, response, body)->

           expect(response.statusCode).toEqual(200)
           done()

I have looked into several mocking libraries for node.js ( https://github.com/easternbloc/Syringe , https://github.com/felixge/node-sandboxed-module ) but so far no success. Whatever i try in the spec, the account.register always get executed. Is this whole approach flawed?

I am using mocha as the test framework and sinon for mocking, stubing and spying. I would suggest you delegate your account module to the auth.coffee module and mock it like so:

exports.init = function (account) {
    // set account object
}

so from the mocha test you can then create a dummy account object and mock it with sinon in the actual test.

describe('some tests', function () {

    var account, response, testObject;

    beforeEach(function () {

        account = {
             register: function () { }
        };

        response = {
            send: function () { }
        };

        testObject = require('./auth');
        testObject.init(account);
    });

    it('should test something', function () {

        var req = { body: { email: ..., password: .... } }, // the request to test
            resMock = sinon.mock(response),
            registerStub = sinon.stub(account, 'register');

        // the request expectations
        resMock.expect('send').once().withArgs(200);

        // the stub for the register method to have some process
        registerStub.once().withArgs('someargs');

        testObject.auth(req. response);

        resMock.verify();

    });

});

Sorry for not writing it down in coffescript but I am not used to it.

Stefan's solution works. I just add some details.

    describe 'register', ->
    account = response = routes_auth = null

    beforeEach ->
        account =
            register: (email, pw, callback)-> 
                if email is 'valid@email.com'
                    callback(null, 1)
                else
                    err = 'error'
                    callback(err, 0)

        response = 
            send: -> {}

        routes_auth = require('../routes/auth').init(account)


    it 'should register a user with email and pw', (done)->
        req =
            body:
                email: 'valid@email.com'
                password: 'pw'

        resMock = sinon.mock(response)
        resMock.expects('send').once().withArgs(200)
        routes_auth.post_register(req, response)
        resMock.verify() 
        done()



    it 'should not register a user without email', ()->
        req =
            body:             
                password: 'pw'

        resMock = sinon.mock(response)
        resMock.expects('send').once().withArgs(400)
        routes_auth.post_register(req, response)
        resMock.verify() 

and the routes/auth.coffee module ...

exports.init = (account)->
    get_available: (req, res)->
        email = req.param.email
        if not email? or email.length < 1
            res.send 400
            return
        account.available email, (err, doc)->
            console.log 'get_available', err, doc
            if err then res.send 401
            else res.send 200


    post_register: (req, res)->
        email = req.body.email
        password = req.body.password
        if email and password
            account.register email, password, (err, doc)->
                if err then res.send 401
                else res.send 200
        else
            res.send 400

I've been using gently for mocking and stubbing and mocha for test framework, and should.js for BDD style of tests. Here is how a sample unit test for me look like:

describe('#Store() ', function () {
    it('will delegate the store to the CacheItem and CacheKey', function () {
        var actualCacheKey, actualConnMgr, actualConfig, actualLogger, actualRequest;
        var actualKeyRequest, actualKeyConfig;

        gently.expect(
            CacheKey, 'CreateInstance', function (apiRequest, config) {
                actualKeyRequest = apiRequest;
                actualKeyConfig = config;

                return mockCacheKey;
            });

        gently.expect(
            CacheItem, 'CreateInstance', function (cacheKey, connectionManager, config, logger, apiRequest) {
                actualCacheKey = cacheKey;
                actualConnMgr = connectionManager;
                actualConfig = config;
                actualLogger = logger;
                actualRequest = apiRequest;

                return mockCacheItem;
            });

        var actualApiRequest, actualCallback;
        gently.expect(mockCacheItem, 'Store', function (request, callback) {
            actualApiRequest = request;
            actualCallback = callback;
        });

        var callback = function () {};
        var apiResponse = {'item': 'this is a sample response from SAS'};
        Cache.GetInstance(connMgr, config, logger).Store(apiRequest, apiResponse, callback);

        mockCacheKey.should.be.equal(actualCacheKey, 'The cachkeKey to CacheItem.CreateIntsance() did not match');
        connMgr.should.be.equal(
            actualConnMgr, 'The connection manager to CacheItem.CreateInstance() did not match');
        config.should.be.equal(actualConfig, 'The config to CacheItem.CreateInstance() did not match');
        logger.should.be.equal(actualLogger, 'The logger to CacheItem.Createinstance did not match');
        apiRequest.should.be.equal(actualRequest, 'The request to CacheItem.Createinstance() did not match');

        apiRequest.should.be.equal(actualKeyRequest, 'The request to CacheKey.CreateInstance() did not match');
        config.should.be.equal(actualKeyConfig, 'The config to CacheKey.CreateInstance() did not match');

        callback.should.be.equal(actualCallback, 'The callback passed to CacheItem.Store() did not match');
        apiResponse.should.be.equal(actualApiRequest, 'The apiRequest passed to CacheItem.Store() did not match');
    });
});

I recommend proxyquire .

It does what you want to achieve, while not relying on dependency injection, which is obtrusive for your code and requires code changes if you didn't write your modules in that fashion.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM