假设我的控制器类中有一个方法可以更新 Redis 数据库中键值对的分数。我想编写一个单元测试来检查分数是否不为空并且增加 1。我只是想看看单元测试如何与 Redis 一起工作,以及如何从特定的键、值对中提取分数并检查其有效性。
Controller 类 //当用户刷新 api/do/v1/togglelike/{id}" 时,redis 中会更新该用户的分数,并加 1;
[HttpGet]
[Route("api/do/v1/togglelike/{id}")]
public IHttpActionResult ToggleLike(String id)
{
var currentUser = "mike";
var likeSet = redis1.SortedSetRangeByScore("likes:" + id);
var likeStatus = redis1.SortedSetScore("likes:" + id, currentUser);
//Current user has not yet liked the profile
if (likeStatus == null)
{
redis1.SortedSetAdd("likes:" + id, currentUser, 1);
return Ok("Like Added");
}
/*redis1.SortedSetAdd("likes:" + id, currentUser, 1);
return Ok("Like Added");*/
else
{
double counter = redis1.SortedSetIncrement("likes:" + id, currentUser, 1);
redis1.SortedSetAdd("likes:" + id, currentUser, counter);
return Ok("Like Added");
/*redis1.SortedSetRemove("likes:" + id, currentUser);
return Ok("Like Removed");*/
}
}
测试类:我想从键值对中获取分数并检查它是否等于有效数字;
namespace VideoControllerTest
{
[TestClass]
public class VideoControllerTest
{
IDatabase redis1;
public VideoControllerTest()
{
redis1 = RedisFactory.Connection.GetDatabase();
}
[TestMethod]
public void VideoController_Adview()
{
//Arrange
VideoController controller = new VideoController();
//Act
IHttpActionResult actionResult = controller.ToggleLike("video123");
//Assert; Check to see the counter is incremented by 1 and is not null;
}
}
}
模拟 Redis 非常适合测试下一层,但这是一件复杂的事情,您可能会花费大量时间来测试模拟而不是生产实现。
而是在运行测试的任何位置设置 Redis 的本地实例,为键添加前缀,然后使用
SCAN
来清理它们。所以(在测试中使用 StackExchange.Redis)...
// In your test setup
VideoController.Prefix = "test:";
// Then create your testing data in Redis
// In your code include the prefix
var likeSet = redis1.SortedSetRangeByScore(Prefix + "likes:" + id);
// In your test cleanup:
// Get the server, as SCAN/KEYS will only run across one server
var server = redis.GetServer("localhost", 6379);
// Get all the keys with the test prefix
var testKeys = server.Keys(pattern: "test:*");
// Delete all the keys, with wait because we don't want fire&forget
Task.WaitAll((from k in testKeys select db.KeyDeleteAsync(k)).ToArray());
这在生产中是非常糟糕的做法,因为
KEYS
或SCAN
操作只会在一台Redis服务器上运行,KEYS
将同步运行并在运行时阻塞。不过,在单元测试中应该没问题,只要您不在同一位置放置生产 Redis。
上面的答案反映了我在 2016 年是如何做到这一点的,但对于现在的大多数测试用例,我模拟了我正在使用的 Redis 连接库(特别是
StackExchange.Redis
):
using StackExchange.Redis;
using NSubstitute; // But you can use whatever you prefer
...
/// <summary>Create a mock Redis connection, which wraps an in-memory dictionary.</summary>
public static IConnectionMultiplexer MockRedisConnection()
{
var dict = new Dictionary<string, RedisValue>(StringComparer.OrdinalIgnoreCase);
var mockDatabase = Substitute.For<IDatabase>();
mockDatabase.StringGetAsync(Arg.Any<RedisKey>(), Arg.Any<CommandFlags>()).
Returns(
context => dict.TryGetValue((string?) context.Arg<RedisKey>() ?? "empty", out var val) ? val : RedisValue.Null);
mockDatabase.StringGetAsync(Arg.Any<RedisKey[]>(), Arg.Any<CommandFlags>()).
Returns(
context =>
[..
from key in context.Arg<RedisKey[]>()
select dict.TryGetValue((string?) key ?? "empty", out var val) ? val : RedisValue.Null
]);
mockDatabase.StringSetAsync(Arg.Any<RedisKey>(), Arg.Any<RedisValue>(), Arg.Any<TimeSpan?>(), Arg.Any<When>(), Arg.Any<CommandFlags>()).
Returns(
context => {
var k = context.Arg<RedisKey>();
var v = context.Arg<RedisValue>();
dict[(string?)k ?? "empty"] = v;
return true;
});
mockDatabase.StringSetAsync(Arg.Any<RedisKey>(), Arg.Any<RedisValue>(), Arg.Any<TimeSpan?>(), Arg.Any<bool>(), Arg.Any<When>(), Arg.Any<CommandFlags>()).
Returns(
context => {
var k = context.Arg<RedisKey>();
var v = context.Arg<RedisValue>();
dict[(string?)k ?? "empty"] = v;
return true;
});
mockDatabase.StringSetAsync(Arg.Any<KeyValuePair<RedisKey, RedisValue>[]>(), Arg.Any<When>(), Arg.Any<CommandFlags>()).
Returns(
context => {
var pairs = context.Arg<KeyValuePair<RedisKey, RedisValue>[]>();
foreach(var (k, v) in pairs)
dict[(string?)k ?? "empty"] = v;
return true;
});
mockDatabase.KeyDeleteAsync(Arg.Any<RedisKey>(), Arg.Any<CommandFlags>()).
Returns(
context => dict.Remove((string?)context.Arg<RedisKey>() ?? "empty"));
mockDatabase.KeyExists(Arg.Any<RedisKey>(), Arg.Any<CommandFlags>()).
Returns(
context => dict.Remove((string?)context.Arg<RedisKey>() ?? "empty"));
// Add any other actions you need here
var mockConnection = Substitute.For<IConnectionMultiplexer>();
mockConnection.GetDatabase(Arg.Any<int>(), Arg.Any<object>()).
Returns(mockDatabase);
return mockConnection;
}
然后我不测试
StackExchange.Redis
或 Redis,我只是使用常规 Dictionary
并确保我的代码已将内容放入或取出。
为了能够对外部系统(在本例中为 Redis 数据库)进行单元测试,您必须模拟外部系统。
如果
redis1
是一个接口,你可以很容易地用 Mock 这样的框架来模拟它,如果它是一个实现,那就很难了,你必须用你自己的类包装它才能模拟它。
您需要将 IDatabase 传递到控制器中,因此我添加了另一个构造函数。
class VideoController
{
private IDatabase redis1;
public VideoController(IDatabase db)
{
this.redis1 = db;
}
}
测试方法应如下
//note : library used for mocking is moq (https://github.com/Moq/moq4)
[TestMethod]
public void VideoController_Adview()
{
//Arrange
Mock<IDatabase> mockRedis = new Mock<IDatabase>();
//set existing score as null
mockRedis.Setup(r => r.SortedSetScore(It.isAny<string>,It.isAny<string>)).Returns(null);
VideoController controller = new VideoController(mockRedis.Object);
//Act
IHttpActionResult actionResult = controller.ToggleLike("video123");
//verify added once
mockRedis.Verify(r => r.SortedSetAdd("likes:videio123",1), Times.Once());
}