Trusting a Fake: Why Use Fakeredis

Sun 18 November 2012 by James Saryerwinnie

Now that fakeredis 0.3.0 is out I think it's a good time to discuss the finer points of fakeredis, and why you should consider using it for your redis unit testing needs.

What exactly is fakeredis? Other than the pedantic naming of "fake" instead of "mock", it is an in memory implementation of the redis client used for python. This allows you to write tests that use the redis-py client interface without having to have redis running.

Setting up redis is not hard, even compiling from source is easy; there's not even a ./configure step! But unit tests should require no configuration to run. Someone should be able to checkout/clone the repo, and be able to run your unit tests.

There's one big problem with writing fakes:

How do you know your fake implementation matches the real implementation?

Fakeredis verifies this in a simple way. First, there's unit tests for fakeredis. And for every unit test for fakeredis, there's the equivalent integration test that actually talks to a real redis server. This ensures that every single test for fakeredis has the exact same behavior as real redis. There's nothing worse than writing unit tests against a fake implementation only to find out that the real implementation is actually different!

In fakeredis, this is implemented with a factory method pattern. The fakeredis tests instantiate a fakeredis.FakeRedis class while the real redis integration tests instantiate a redis.Redis instance:

class TestFakeRedis(unittest.TestCase):
    def setUp(self):
        self.redis = self.create_redis()

    def create_redis(self, db=0):
        return fakeredis.FakeStrictRedis(db=db)

    def test_set_then_get(self):
        self.assertEqual(self.redis.set('foo', 'bar'), True)
        self.assertEqual(self.redis.get('foo'), 'bar')


class TestRealRedis(TestFakeRedis):
    def create_redis(self, db=0):
        return redis.Redis('localhost', port=6379, db=db)

Now every test written in the TestFakeRedis class will be automatically run against both a FakeRedis instance and a Redis instance, ensuring parity between the two.

This also makes it easier for contributors. If they notice an inconsistency between fakeredis and redis, they only need to write a single test and they'll have a simple repro that shows that the test passes for redis but fails against FakeRedis.

And finally test coverage. Every single implemented command in fakeredis has test cases. I only accept contributions for bug fixes/new features if they have tests. I normally don't worry about actual coverage numbers, but out of curiosity I checked what those numbers actually were:

$ coverage report fakeredis.py
Name        Stmts   Miss  Cover
-------------------------------
fakeredis     640     19    97%

Not bad. Most of the missing lines are either unimplemented commands (pass statements counted as missing coverage) or precondition checks such as:

def zadd(self, name, *args, **kwargs):
   # ...
   if len(args) % 2 != 0:
       raise redis.RedisError("ZADD requires an equal number of "
                              "values and scores")

I do plan on going through and ensuring that all of these precondition checks have tests.

So the next time you're looking for a fake implementation of redis, consider fakeredis.


Comments