如何使用 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 : ICacheHandler
和 ICacheHandler
。您的所有代码只会与 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);
}
我正在努力模拟与 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 : ICacheHandler
和 ICacheHandler
。您的所有代码只会与 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);
}