Reputation: 4440
I want to ensure that the method I am testing makes two calls to a mocked method using the same values for certain arguments. These values are generated inside the method being tested so are not known when the test is set up. In this case the method being tested is storing two related items in Redis, but this is not relevant to the question; it's the mocking that I'm asking about. I want to confirm that the way I've done it, which does work, is the best way to do it. Perhaps there is some other feature of Moq that I have missed that could let this be done in a better way.
This is what I have.
IRedisClient _redisClientMock = new Mock<IRedisClient>(MockBehavior.Strict);
string token = null;
DateTime expires = default(DateTime);
_redisClientMock
.Setup(x => x.Set(KEY_PREFIX, It.IsAny<string>(), $"{TEST_ID},{TEST_NAME}", It.IsAny<DateTime>()))
.Callback<string, string, string, DateTime?>((p, k, v, e) =>
{
token = k;
expires = e.Value;
});
_redisClientMock
.Setup(x => x.Set(KEY_PREFIX, TEST_ID, It.IsAny<string>(), It.IsAny<DateTime>()))
.Callback<string, string, string, DateTime?>((p, k, v, e) =>
{
if (!v.Equals(token, StringComparison.InvariantCultureIgnoreCase))
throw new Exception($"RedisClient.Set expected value {token} but received {v}");
if (!e.Value.Equals(expires))
throw new Exception($"RedisClient.Set expected expires {expires} but received {e}");
});
The fact that I've had to use exceptions in a callback seems a bit clunky, hence my wondering if there is a better way to verify this using mocking.
Here is an example of the code under test, a private method called by the method actually being tested.
string StoreClientDetailsInRedisAndReturnToken(string clientId, string clientName)
{
string token = Guid.NewGuid().ToString("N");
string data = $"{clientId},{clientName}";
DateTime expires = DateTime.UtcNow.AddDays(AdminSettings.Current.ExpiryDays);
RedisClient.Set(KeyPrefix, token, data, expires);
RedisClient.Set(KeyPrefix, clientId, token, expires);
return token;
}
The token is included in an activation link sent out in an email, so we need to be able to retrieve the data using the token. The second Redis set is allowing us to also retrieve the token using the clientId, in order to confirm that the request is still pending. i.e. the link has not been clicked and processed and the Redis entries have not expired and been removed.
I'm sure that you will want to suggest other ways to write the code under test but writing this I can already picture other ways to do it. What I would really like to know from this question is whether or not Moq allows one to verify that two methods, related in some way, are both called in a particular sequence.
Upvotes: 0
Views: 136
Reputation: 9499
This is how i would test this method. It is all fairly straightforward and asserts on all the behaviours you have specified in your code.
[TestMethod]
public void StoreClientDetailsInRedisAndReturnToken_SetsTwoValuesInRedis_ReturnsGuid()
{
var data = new List<dynamic>()
var redis = new Mock<IRedisClient>();
redis.Setup(x => x.Set(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>()).Callback((string kp, string token, string data, DateTime exp) => data.Add(new {KeyPrefix = kp, Key = token, Data = data, Expiry = exp);
var target = new MyClass(redis.Object);
var result = target.StoreClientDetailsInRedisAndReturnToken("funny", "banana");
new Guid(result); // do nothing, will throw if result is not parseable as Guid
Assert.AreEqual(2, data.Count())
Assert.AreEqual(KeyPrefix, data.FirstOrDefault().KeyPrefix);
Assert.AreEqual(result, data.FirstOrDefault().Key);
Assert.AreEqual("funny,banana", data.FirstOrDefault().Data);
// here if you have enterprise edition of visual studio you can use microsoft fakes to actually test it, or otherwise
Assert.IsTrue(((DateTime)data.FirstOrDefault().Expiry) > DateTime.UtcNow.AddDays(AdminSettings.Current.ExpiryDays).AddMinutes(-1));
Assert.AreEqual(KeyPrefix, data.LastOrDefault().KeyPrefix);
Assert.AreEqual("funny", data.LastOrDefault().Key);
Assert.AreEqual(result, data.LastOrDefault().Data);
Assert.IsTrue(((DateTime)data.LastOrDefault().Expiry) > DateTime.UtcNow.AddDays(AdminSettings.Current.ExpiryDays).AddMinutes(-1));
}
Upvotes: 1