Sinstein
Sinstein

Reputation: 909

How to test Redis Lock via Rspec

We have a Lockable concern that allows for locks via Redis

module Lockable
  extend ActiveSupport::Concern

  def redis_lock(key, options = {})
    Redis::Lock.new(
      key,
      expiration: options[:expiration] || 15,
      timeout: options[:timeout] || 0.1
    ).lock { yield if block_given? }
  end
end

We use this in a Controller method to ensure concurrent requests are handled correctly.

def create
  redis_lock(<generated_key>, timeout: 15) do
    # perform_operation
  end

  render json: <data>, status: :ok
end

When testing this action, I want to test that the correct generated_key is being sent to Redis to initiate a lock.

I set up an expect for the Redis::Lock but that returns false always presumably because the request to create is sent mid request and not at the end of it.

expect(Redis::Lock).to receive(:create).once

Test structure:

context 'return status ok' do
       When do
          post :create, params: {
            <params>
          }
        end
        Then {
          expect(Redis::Lock).to receive(:create).once
          response.ok?
        }
   end
end

Since the lock is cleared at the end of the method call, I cannot check for the key in redis as a test.

This answer recommends setting up a fake class that matches the structure of Lockable to emulate the same behaviour but how do I write a test for it? The method we have does not return any value to verify.

Upvotes: 2

Views: 2263

Answers (2)

Tachyons
Tachyons

Reputation: 2171

This is modified version of davegson using Rspec spies, This eliminates the coding smells like any_instances_of

describe 'Lockable' do
  describe '#redis_lock' do
    it "delegates functionality to Redis::Lock with proper arguments" do
      # create an instance spy
      redis_lock = instance_spy("Redis::Lock")
      expect(Redis::Lock).to receive(:new).with('test', any_args).and_return(redis_lock)
      redis_lock('test', timeout: 15) do
        sleep 1
      end
      expect(redis_lock).to have_received(:lock)
    end
  end
end

Upvotes: 1

davegson
davegson

Reputation: 8331

From the code you provided I believe you just set up the wrong test:

expect(Redis::Lock).to receive(:create).once

This expects the Redis::Lock class to receive a create call, but you are calling create in your controller.

What you are doing in the redis_lock method is initializing an instance of Redis::Lock and calling lock on it. In my opinion, that is what you should test:

expect_any_instance_of(Redis::Lock).to receive(:lock).once

The Implementation would look something like this:

describe 'Lockable' do
  describe '#redis_lock' do
    subject { lockable.redis_lock(key, options) }

    # you gotta set this
    let(:lockable) { xyz }
    let(:key) { xyz } 
    let(:options) { x: 'x', y: 'y' }

    it 'calls Redis::Lock.new with correct arguments' do 
      expect(Redis::Lock).to receive(:new).with(key: key, options: options)
      subject
    end

    it 'calls #lock on the created Redis::Lock instance' do
      expect_any_instance_of(Redis::Lock).to receive(:lock).once
      subject
    end
  end
end

Upvotes: 1

Related Questions