Sam Storie
Sam Storie

Reputation: 4564

How to use Moq to mock up the StackExchange.Redis ConnectionMultiplexer class?

I am working to mock up behaviors related to the StackExchange.Redis library, but can't figure out how to properly mock the sealed classes it uses. A specific example is in my calling code I'm doing something like this:

    var cachable = command as IRedisCacheable;

    if (_cache.Multiplexer.IsConnected == false)
    {
        _logger.Debug("Not using the cache because the connection is not available");

        cacheAvailable = false;
    }
    else if (cachable == null)
    {

The key line in there is _cache.Multiplexer.IsConnected where I'm checking to make sure I have a valid connection before using the cache. So in my tests I want to mock up this behavior with something like this:

    _mockCache = new Mock<IDatabase>();
    _mockCache.Setup(cache => cache.Multiplexer.IsConnected).Returns(false);

However, while that code compiles just fine, I get this error when running the test:

Moq exception thrown within the test

I have also tried mocking the multiplexer class itself, and providing that to my mocked cache, but I run into the fact the multiplexer class is sealed:

    _mockCache = new Mock<IDatabase>();
    var mockMultiplexer = new Mock<ConnectionMultiplexer>();
    mockMultiplexer.Setup(c => c.IsConnected).Returns(false);
    _mockCache.Setup(cache => cache.Multiplexer).Returns(mockMultiplexer.Object);

...but that results in this error:

The error thrown when trying to mock a sealed class

Ultimately I want to control whether that property is true or false in my tests, so is there a correct way to mock up something like this?

Upvotes: 20

Views: 24963

Answers (4)

Ben Wesson
Ben Wesson

Reputation: 639

I have solved this problem by using a connection provider class to create the instance of the ConnectionMultiplexer. The connection provider class can simply be injected into your cache service. The benefit of this approach is that the connection provider is the only code not tested (basically a single line of someone else's code) and your cache service can be tested by mocking the injected interfaces as normal.

In the code below my cache service can be tested and only the connection provider class needs to be excluded from code coverage.

public interface IElastiCacheService
{
    Task<string> GetAsync(string key);

    Task SetAsync(string key, string value, TimeSpan expiry);
}

public class ElastiCacheService : IElastiCacheService
{
    private readonly ElastiCacheConfig _config;
    private readonly IConnectionMultiplexer _connection = null;

    public ElastiCacheService(
        IOptions<ElastiCacheConfig> options,
        IElastiCacheConnectionProvider connectionProvider)
    {
        _config = options.Value;
        _connection = connectionProvider.GetConnection(_config.FullAddress);
    }

    public async Task<string> GetAsync(string key)
    {
        var value = await _connection.GetDatabase().StringGetAsync(key, CommandFlags.PreferReplica);
        return value.IsNullOrEmpty ? null : value.ToString();
    }

    public Task SetAsync(string key, string value, TimeSpan expiry) =>
        _connection.GetDatabase().StringSetAsync(key, value, expiry);
}

public interface IElastiCacheConnectionProvider
{
    IConnectionMultiplexer GetConnection(string endPoint);
}

[ExcludeFromCodeCoverage]
public class ElastiCacheConnectionProvider : IElastiCacheConnectionProvider
{
    public IConnectionMultiplexer GetConnection(string endPoint) =>
        ConnectionMultiplexer.Connect(endPoint);
}

Upvotes: 0

Dan Sabin
Dan Sabin

Reputation: 21

Not included in the above answer is the more detailed Setup of the mockDatabase instance. I struggled a little bit finding a working example of something as simple as mocking the IDatabase StringGet method (e.g., handling of optional parameters, using RedisKey vs string, using RedisValue vs string, etc.), so thought I would share. Here is what worked for me.

This test setup:

var expected = "blah";
RedisValue expectedValue = expected;

mockDatabase.Setup(db => db.StringGet(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.Returns(expectedValue);

To affect what is returned by this tested method call:

var redisValue = _connectionMultiplexer.GetDatabase().StringGet(key);

Upvotes: 2

Andriy Tolstoy
Andriy Tolstoy

Reputation: 6090

Use the interface IConnectionMultiplexer instead of the concrete class ConnectionMultiplexer in your own class.

public interface ICacheable
{
   void DoYourJob();
}

public sealed class RedisCacheHandler : ICacheable
{
    private readonly IConnectionMultiplexer multiplexer;

    public RedisCacheHandler(IConnectionMultiplexer multiplexer)
    {
        this.multiplexer = multiplexer;
    }

    public void DoYourJob() 
    {
        var database = multiplexer.GetDatabase(1);

        // your code        
    }
}

Then you could easily mock and test it:

// Arrange
var mockMultiplexer = new Mock<IConnectionMultiplexer>();

mockMultiplexer.Setup(_ => _.IsConnected).Returns(false);

var mockDatabase = new Mock<IDatabase>();

mockMultiplexer
    .Setup(_ => _.GetDatabase(It.IsAny<int>(), It.IsAny<object>()))
    .Returns(mockDatabase.Object);

var cacheHandler = new RedisCacheHandler(mockMultiplexer.Object);

// Act
cacheHandler.DoYourJob();


// Assert
// your tests

Upvotes: 22

Haney
Haney

Reputation: 34832

The best approach in my opinion is to wrap all of your Redis interaction in your own class and interface. Something like CacheHandler : ICacheHandler and ICacheHandler. All of your code would only ever speak to ICacheHandler.

This way, you eliminate a hard dependency on Redis (you can swap out the implementation of ICacheHandler as you please). You can also mock all interaction with your caching layer because it's programmed against the interface.

You should not test StackExchange.Redis directly - it is not code you've written.

Upvotes: 19

Related Questions