Saturday, June 28, 2014

Faking Redis in Nodejs with Fakeredis

You probably know what redis is. Just in case it is a data structure server that usually work as a key value pair store. The data is stored in memory by default instead of a disk, so it is really really fast with the sacrifice of reliability.

Faking


At work while writing tests for some code that uses redis, in nodejs, I wanted to fake a redis server. Why would I do such thing? Because my tests are better isolated. They don't depend on some other process (the redis server).

Now how to fake the behavior of redis for tests? By using fantastic fakeredis. fakeredis is an npm library which makes faking the redis behavior in a nodejs application very easy. It also has a ruby gem, so can be used in a ruby application as well.

The code under tests


This is the code I wanted to test.

var redis = require('redis');

var client = redis.createClient();

function isOnline(username, callback){
  client.exists(username + 'SocketId', function(err, exists){
    callback(err, !!exists);
  });
}

exports.isOnline = isOnline;

isOnline is a simple function that checks whether a socket id for a given user name exists in the redis store.

Tests Without Faking


My usual test for this function, if I didn't knew fakeredis existed, would be,

var redis = require('redis');
var assert = require('assert');

var users = require('../lib/users.js')

var client = redis.createClient();

describe('isOnline', function(){
  it('should return true if user has a socket id', function(done){
    //add a mock key value pair
    client.set('onlineUserSocketId', 'some_socket_id', function(){ 

      users.isOnline('onlineUser', function(err, online){
        assert(online);
        done();
      });

    });
  });

  it('should return false if user does not have a socket id', function(done){
    users.isOnline('offlineUser', function(err, online){
      assert(!online);
      done();
    });
  });
});

As it is obvious by the code itself I am testing whether the function returns true to the callback when a socket id for a particular user name is in the redis store, and false otherwise.

The biggest problem here is that anyone who runs those tests must make sure a redis-server process is running. Tests won't pass otherwise. Also running these tests on the production server is probably a bad idea since they would tamper with production data. Also after each test their should be a clean up step that clears out the data added to the database during the test. A flushdb call, which flushes out the data, would be ideal for this task. As every test would start to run on a clean database, they will be better isolated from each other. But such call is hazardous because someone could run the tests on a production server and flush out all the production data!

Tests With faking


Here's how fakeredis is used to avoid above shortcomings.

I used sinon to stub the redis createClient function. Before all here is the before hook that does it.

var fakeredis = require('fakeredis');
var sinon = require('sinon');
var assert = require('assert');

var users;

var client;

describe('isOnline', function(){

  before(function(){
    sinon.stub(redis, 'createClient', fakeredis.createClient);
    users = require('../lib/users.js');
    client = redis.createClient();
  });

});

The block of code in the before hook would run before running any test and would stub the regulare redis.createClient function with fakeredis.createClient function. Stubbing is replacing a resource that your system uses, with something that imitates that resource's behavior. So after stubbing whenever the redis.createClient is called what actually gets called is fakeredis.createClient.

Now any method call on the redis client instance returned by createClient function would not tamper with anything in the redis server. fakeredis would imitate the behavior of and the code under tests would not know a thing about the faking! Now the tests look like this.

var redis = require('redis');
var fakeredis = require('fakeredis');
var sinon = require('sinon');
var assert = require('assert');

var users;

var client;

describe('isOnline', function(){

  before(function(){
    sinon.stub(redis, 'createClient', fakeredis.createClient);
    users = require('../lib/users.js');
    client = redis.createClient();
  })

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

  afterEach(function(done){
    client.flushdb(function(err){
      done();
    });
  })

  it('should return true if user has a socket id.', function(done){
    client.set('onlineUserSocketId', 'some_socket_id', function(){
      users.isOnline('onlineUser', function(err, online){
        assert(online);
        done();
      });
    });
  });

  it('should return false if user does not have a socket id.', function(done){
    users.isOnline('offlineUser', function(err, online){
      assert(!online);
      done();
    });
  });
});

As you can see I have added an after and an afterEach block. What the after block does should be obvious. It brings the redis.createClient function to the previous state.

The afterEach block is more interresting. It adds the clean up step I stated earlier. It calls client.flushdb without hesitation because the the client object would only work with fakeredis data. As the name implies afterEach hook is called after each test so that it makes sure the next test is run on a fresh store.

No comments:

Post a Comment