Search code examples
ruby-on-railsredisrspec-rails

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.


Solution

  • 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