简体   繁体   中英

node jasmine - how to unit test on function that involve redis call?

I just started playing around Jasmine and I'm still struggling on the spyon/mocking things, eg, I have a function

module.exports = (() => {
    ....

    function getUserInfo(id) {
        return new Promise((resolve, reject) => {
            redis.getAsync(id).then(result => {
                resolve(result)
            })
        }) 
    }
    return { getUserInfo: getUserInfo }
})()

Then I start writing the Jasmine spec

describe('Test user helper', () => {
    let userInfo

    beforeEach(done => {
        userHelper.getUserInfo('userid123')
            .then(info => {
                userInfo = info
                done() 
            })
    })

    it('return user info if user is found', () => {
        expect(userInfo).toEqual('info of userid 123')
    })
})

It runs well, but my question is how can I mock the redis.getAsync call, so it can become a real isolated unit test?

Thanks.

Good question. You can mock out the redis dependency but only if you rewrite you code, slightly, to be more testable. Here, that means making redis a parameter to the factory that returns the object containing getUserInfo .

Of course, this changes the API, callers now need to call the export to get the object. To fix this, we can create a wrapper module that calls the function with the standard redis object, and returns the result. Then we move the actual factory into an inner module, which still allows it to be tested.

Here is what that might well look like

user-helper/factory.js

module.exports = redis => {
  ....

  function getUserInfo(id) {
    return redis.getAsync(id); // note simplified as new Promise was not needed
  }
  return {getUserInfo};
};

user-helper/index.js

// this is the wrapper that preserves existing API
module.exports = require('./factory')(redis);

And now for the test

const userHelperFactory = require('./user-helper/factory');

function createMockRedis() {
  const users = [
    {userId: 'userid123'},
    // etc.
  ];
  return {
    getAsync: function (id) {
      // Note: I do not know off hand what redis returns, or if it throws,
      // if there is no matching record - adjust this to match.
      return Promise.resolve(users.find(user => user.userId === id));
    }
  };
}

describe('Test user helper', () => {
  const mockRedis = createMockRedis();
  const userHelper = userHelperFactory(mockRedis);

  let userInfo;

  beforeEach(async () => {
    userInfo = await userHelper.getUserInfo('userid123');
  });

  it('must return user info when a matching user exists', () => {
    expect(userInfo).toEqual('info of userid 123');
  });
});

NOTE: As discussed in comments, this was just my incidental approach to the situation at hand. There are plenty of other setups and conventions you can use but the primary idea was just based on the existing export of the result of an IIFE, which is a solid pattern, and I leveraged the NodeJS /index convention to preserve the existing API. You could also use one file and export via both module.exports = factory(redis) and module.exports.factory = factory , but that would, I believe, be less idiomatic in NodeJS. The broader point was that being able to mock for tests, and testability in general is just about parameterization.

Parameterization is wonderfully powerful, and its simplicity is why developers working in functional languages sometimes laugh at OOP programmers, such as yours truly, and our clandestine incantations like "Oh glorious Dependency Injection Container, bequeath unto me an instanceof X" :)

It is not that OOP or DI get it wrong it is that testability, DI, IOC, etc. are just about parameterization.

Interestingly, if we were loading redis as a module, and if we were using a configurable module loader, such as SystemJS, we could do this by simply using loader configuration at the test level. Even Webpack lets you do this to some extent, but for NodeJS you would need to monkey patch the Require Function, or create a bunch of fake packages, which are not good options.

To the OP's specific response

Thanks! That's a good idea, but practically, it seems it's quite strange when I have tons of file to test in which I will need to create a factory and index.js for each of them.

You would need to restructure your API surface and simply export factories that consuming code must call, rather than the result of applying those factories, to reduce the burden, but there are tradeoffs and default instances are helpful to consumers.

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