如何使用 Moq 模拟 StackExchange.Redis ConnectionMultiplexer class?

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

我正在努力模拟与 StackExchange.Redis 库相关的行为,但无法弄清楚如何正确模拟它使用的密封 classes。一个具体的例子在我的调用代码中我正在做这样的事情:

    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)
    {

其中的关键行是 _cache.Multiplexer.IsConnected,我在其中检查以确保在使用缓存之前有有效的连接。所以在我的测试中,我想用这样的东西模拟这种行为:

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

然而,虽然该代码编译得很好,但在 运行 进行测试时出现此错误:

我也试过模拟多路复用器 class 本身,并将其提供给我的模拟缓存,但我 运行 了解到多路复用器 class 是密封的:

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

...但这会导致此错误:

最终我想在我的测试中控制 属性 是真还是假,那么有没有正确的方法来模拟这样的东西?

我认为最好的方法是将所有 Redis 交互包装在您自己的 class 和界面中。类似于 CacheHandler : ICacheHandlerICacheHandler。您的所有代码只会与 ICacheHandler.

对话

这样,您就消除了对 Redis 的硬依赖(您可以根据需要交换 ICacheHandler 的实现)。您还可以模拟与缓存层的所有交互,因为它是针对接口编程的。

您不应该直接测试 StackExchange.Redis - 它不是您编写的代码。

在您自己的 class 中使用界面 IConnectionMultiplexer instead of the concrete class ConnectionMultiplexer

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        
    }
}

然后你就可以轻松地模拟和测试它了:

// 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

上面的答案中没有包含 mockDatabase 实例的更详细的设置。我努力寻找一个像模拟 IDatabase StringGet 方法一样简单的工作示例(例如,处理可选参数,使用 RedisKey 与字符串,使用 RedisValue 与字符串等),所以我想分享一下。这是对我有用的。

此测试设置:

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

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

要影响此测试方法调用返回的内容:

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

我已经通过使用连接提供程序 class 创建 ConnectionMultiplexer 的实例解决了这个问题。连接提供程序 class 可以简单地注入到您的缓存服务中。这种方法的好处是连接提供程序是唯一未测试的代码(基本上是别人的一行代码),您的缓存服务可以通过正常模拟注入的接口来测试。

在下面的代码中可以测试我的缓存服务,只有连接提供程序 class 需要从代码覆盖范围中排除。

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);
}