RSpec & Redis

Redis is a bucket, a dump or just a key value store. It is quite convenient for caching.
When testing business logic on top of it, some issues should be considered.

Redis business logic

A simple Redis wrapper is used as an examplary implementation for some business logic based on Redis.
The wrappers goal is to persist objects into Redis, but with a unique key.
One use case could be different kind of ActiveRecord objects with the same ID. They can not be dumped into Redis in a unique way per se. That wrapper builds a distinct key consisting of the objects class name and ID:

module Model
  class Cache
    attr_reader :record

    def initialize(record)
      @record = record
    end

    def dump(value)
      redis.set key, value 
    end

    def find
      redis.get key
    end

    private

    def key
      "#{record.class}:#{record.id}"
    end

    def redis
      @redis ||= Redis.new
    end
  end
end

Based on the distinct key, the wrapper can persist and find a value.
The same approach is used by similar existing Ruby Gems.
This implementation has to be tested with RSpec.

Redis spec helper

At first it makes sense to add a Redis spec helper, that cares about repeating tasks, which are not necessary for the specification readability. A minimum redis_spec_helper.rb:

RSpec.configure do |config|
  config.before(:each) { redis.flushdb }
  config.after(:each) { redis.quit }
end

It flushes all Redis storages before each test and quits the connection to Redis afterwards.
It also could include setting a dedicated test Redis database instance or other test helpers.

Unit test preparations for Redis

The unit test demands a few test preparations. In particular the testable Model::Cache object as the subject:

require 'rspec'
require 'redis_helper'

TestModel = Struct.new :id

RSpec.describe Model::Cache do
  let!(:redis) { Redis.new }
  let!(:model) { TestModel.new rand(1..10) }
  let!(:model_key) { "TestModel:#{model.id}" }

  subject { Model::Cache.new model }

  describe '#model' do
    it 'returns the model' do
      expect(subject.model).to be model
    end
  end
end

Furthermore several variables are created, namely:

  • redis (an independent Redis database access)
  • model (a Struct object with a random ID, playing the model)
  • model_key (the expected model key)

After all the model accessor model is tested already.

Unit tests with Redis

At least the tests for both methods dump and find cover the expected behaviour:

require 'rspec'
require 'redis_helper'

TestModel = Struct.new :id

RSpec.describe Model::Cache do
  let!(:redis) { Redis.new }
  let!(:model) { TestModel.new rand(1..10) }
  let!(:model_key) { "TestModel:#{model.id}" }

  subject { Model::Cache.new model }

  describe '#model' do
    it 'returns the model' do
      expect(subject.model).to be model
    end
  end

  describe '#dump' do
    before { subject.dump 'test' }

    it 'dumps the models key into Redis' do
      expect(redis.keys).to eq([model_key])
    end

    it 'dumps the value for the models key into Redis' do
      expect(redis.get(model_key)).to eq('test')
    end
  end

  describe '#find' do
    it 'finds the records value' do
      redis.set model_key, 'test'
      expect(subject.find).to eq('test')
    end

    context 'when dumps an Integer' do
      it 'returns the value as a String' do
        redis.set model_key, 123
        expect(subject.find).to eq('123')
      end
    end
  end
end