How to stub a method that is called from an outer

2019-06-24 14:29发布

问题:

I have a Redis client that is created thus using the node_redis library (https://github.com/NodeRedis/node_redis):

var client = require('redis').createClient(6379, 'localhost');

I have a method I want to test whose purpose is to set and publish a value to Redis, so I want to test to ensure the set and publish methods are called or not called according to my expectations. The tricky thing is I want this test to work without needing to fire up an instance of a Redis server, so I can't just create the client because it will throw errors if it cannot detect Redis. Therefore, I need to stub the createClient() method.

Example method:

// require('redis').createClient(port, ip) is called once and the 'client' object is used globally in my module.
module.exports.updateRedis = function (key, oldVal, newVal) {
  if (oldVal != newVal) {
    client.set(key, newVal);
    client.publish(key + "/notify", newVal);
  }
};

I've tried several ways of testing whether set and publish are called with the expected key and value, but have been unsuccessful. If I try to spy on the methods, I can tell my methods are getting called by running the debugger, but calledOnce is not getting flagged as true for me. If I stub the createClient method to return a fake client, such as:

{
  set: function () { return 'OK'; },
  publish: function () { return 1; }
}

The method under test doesn't appear to be using the fake client.

Right now, my test looks like this:

var key, newVal, oldVal, client, redis;

before(function () {
  key = 'key';
  newVal = 'value';
  oldVal = 'different-value';
  client = {
    set: function () { return 'OK'; },
    publish: function () { return 1; }
  }
  redis = require('redis');
  sinon.stub(redis, 'createClient').returns(client);

  sinon.spy(client, 'set');
  sinon.spy(client, 'publish');
});

after(function () {
  redis.createClient.restore();
});

it('sets and publishes the new value in Redis', function (done) {
  myModule.updateRedis(key, oldVal, newVal);

  expect(client.set.calledOnce).to.equal(true);
  expect(client.publish.calledOnce).to.equal(true);

  done();
});

The above code gives me an Assertion error (I'm using Chai)

AssertionError: expected false to equal true

I also get this error in the console logs, which indicates the client isn't getting stubbed out when the method actually runs.

Error connecting to redis [Error: Ready check failed: Redis connection gone from end event.]

UPDATE

I've since tried stubbing out the createClient method (using the before function so that it runs before my tests) in the outer-most describe block of my test suite with the same result - it appears it doesn't return the fake client when the test actually runs my function.

I've also tried putting my spies in the before of the top-level describe to no avail.

I noticed that when I kill my Redis server, I get connection error messages from Redis, even though this is the only test (at the moment) that touches any code that uses the Redis client. I am aware that this is because I create the client when this NodeJS server starts and Mocha will create an instance of the server app when it executes the tests. I'm supposing right now that the reason this isn't getting stubbed properly is because it's more than just a require, but the createClient() function is being called at app startup, not when I call my function which is under test. I feel there still ought to be a way to stub this dependency, even though it's global and the function being stubbed gets called before my test function.

Other potentially helpful information: I'm using the Gulp task runner - but I don't see how this should affect how the tests run.

回答1:

I ended up using fakeredis(https://github.com/hdachev/fakeredis) to stub out the Redis client BEFORE creating the app in my test suite like so:

var redis = require('fakeredis'),
    konfig = require('konfig'),
    redisClient = redis.createClient(konfig.redis.port, konfig.redis.host);

sinon.stub(require('redis'), 'createClient').returns(redisClient);

var app = require('../../app.js'),
//... and so on

And then I was able to use sinon.spy in the normal way:

describe('some case I want to test' function () {
  before(function () {
    //...
    sinon.spy(redisClient, 'set');
  });

  after(function () {
    redisClient.set.restore();
  });

  it('should behave some way', function () {
    expect(redisClient.set.called).to.equal(true);
  });
});

It's also possible to mock and stub things on the client, which I found better than using the redisErrorClient they provide for testing Redis error handling in the callbacks.

It's quite apparent that I had to resort to a mocking library for Redis to do this because Sinon couldn't stub out the redisClient() method as long as it was being called in an outer scope to the function under test. It makes sense, but it's an annoying restriction.