sds/mock_redis

Use MockRedis with Redis::Connection.drivers

Closed this issue · 3 comments

23tux commented

I'm trying to migrate from fakeredis to mock_redis, because the fakeredis gem is pretty outdated and I guess not maintained anymore. I now face the problem, that we often use Redis.new at the class level like this

class Foo
  # ...
  REDIS = Redis.new(credentials)
  # ...
end

but stubbing out Redis.new in RSpec like this doesn't work, as it is too late and the constant is already set:

config.before do
  allow(Redis).to receive(:new).and_return(MockRedis.new)
end

I wondered how Fakeredis did it, and found this line

https://github.com/magicguitarist/fakeredis/blob/509822e07289a7f78e340b8be757fae662c0e0de/lib/redis/connection/memory.rb#L1601-L1602

This is the same method as redis-rb does it

https://github.com/redis/redis-rb/blob/4e9d73d3bb47831fe720cbce7c47cb11dd3f4de9/lib/redis/connection/ruby.rb#L436-L437

So I was wondering, if I could also use MockRedis like this. Unfortunately, doing this results in an error, when the client is created:

require "redis"
require "mock_redis"
Redis::Connection.drivers << MockRedis
Redis.new.get("*")

=> NoMethodError: undefined method `write' for #<MockRedis::Database:0x000055bbe31c7d08>
/usr/local/bundle/gems/mock_redis-0.34.0/lib/mock_redis/multi_db_wrapper.rb:21:in `method_missing'
/usr/local/bundle/gems/mock_redis-0.34.0/lib/mock_redis/expire_wrapper.rb:17:in `method_missing'
/usr/local/bundle/gems/mock_redis-0.34.0/lib/mock_redis/transaction_wrapper.rb:30:in `method_missing'
/usr/local/bundle/gems/mock_redis-0.34.0/lib/mock_redis/pipelined_wrapper.rb:27:in `method_missing'
/usr/local/bundle/gems/mock_redis-0.34.0/lib/mock_redis.rb:99:in `block in method_missing'
/usr/local/bundle/gems/mock_redis-0.34.0/lib/mock_redis.rb:155:in `logging'
/usr/local/bundle/gems/mock_redis-0.34.0/lib/mock_redis.rb:98:in `method_missing'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:320:in `block in write'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:299:in `io'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:318:in `write'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:276:in `block (3 levels) in process'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:270:in `each'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:270:in `block (2 levels) in process'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:420:in `ensure_connected'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:269:in `block in process'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:356:in `logging'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:268:in `process'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:161:in `call'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:144:in `block in connect'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:344:in `with_reconnect'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:114:in `connect'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:417:in `ensure_connected'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:269:in `block in process'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:356:in `logging'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:268:in `process'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/client.rb:161:in `call'
/usr/local/bundle/gems/redis-4.8.0/lib/redis.rb:270:in `block in send_command'
/usr/local/bundle/gems/redis-4.8.0/lib/redis.rb:269:in `synchronize'
/usr/local/bundle/gems/redis-4.8.0/lib/redis.rb:269:in `send_command'
/usr/local/bundle/gems/redis-4.8.0/lib/redis/commands/strings.rb:191:in `get'

Does anyone know if using Redis::Connection.drivers with redis is possible? The only alternative is trying to convert every constant to a class method, which I'd like to avoid.

sds commented

I don't know the answer to your question, and since this issue has been open a while I'm going to assume anyone who has context on this project doesn't know the answer either. Hope you are able to figure out a solution/workaround.

23tux commented

That's a pity, I still need a solution for this :(

23tux commented

For everyone coming over from FakeRedis, I managed to write a small patch, so that you can simply use Redis.new everywhere in your application, and still have MockRedis in your tests:

# config/initializers/00_mock_redis.rb
if Rails.env.test?
  module MockRedisConnectionsTracker
    def connections
      @connections ||= {}
    end

    def reset!
      @connections = nil
    end

    def flushall
      connections.values.each(&:flushall)
    end

    def for(...)
      mock = new(...)
      connections[mock.id] ||= mock
    end
  end
  MockRedis.extend(MockRedisConnectionsTracker)

  Redis.singleton_class.class_eval do
    define_method(:new) do |*args, **kwargs, &block|
      MockRedis.for(*args, **kwargs, &block)
    end
  end

  Rails.application.reloader.after_class_unload do
    # Code inside an initializer is NOT reloaded on changes in development or test mode.
    # Therefore, @connections would leak old instances from previous states of our code base,
    # because it still holds the old instances of MockRedis.
    # To make it more resilient to code changes during testing, we reset the @connections variable.
    MockRedis.reset!
  end
end

And to reset the DB between test runs use

# spec/spec_helper.rb
RSpec.configure do |config|
  config.before { MockRedis.flushall }
end