在单元测试中模拟 IMemoryCache
Mock IMemoryCache in unit test
我正在使用 asp net core 1.0 和 xunit。
我正在尝试为一些使用 IMemoryCache
的代码编写单元测试。但是,每当我尝试在 IMemoryCache
中设置一个值时,我都会收到 Null 引用错误。
我的单元测试代码是这样的:
IMemoryCache
被注入到我要测试的 class 中。但是,当我尝试在测试中的缓存中设置一个值时,我得到一个空引用。
public Test GetSystemUnderTest()
{
var mockCache = new Mock<IMemoryCache>();
return new Test(mockCache.Object);
}
[Fact]
public void TestCache()
{
var sut = GetSystemUnderTest();
sut.SetCache("key", "value"); //NULL Reference thrown here
}
这是 class 测试...
public class Test
{
private readonly IMemoryCache _memoryCache;
public Test(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public void SetCache(string key, string value)
{
_memoryCache.Set(key, value, new MemoryCacheEntryOptions {SlidingExpiration = TimeSpan.FromHours(1)});
}
}
我的问题是...我需要以某种方式设置 IMemoryCache
吗?为 DefaultValue 设置一个值?当 IMemoryCache
被 Mocked 时,默认值是多少?
IMemoryCache.Set
是一种扩展方法,因此不能使用 Moq 框架进行模拟。
扩展代码可用 here
public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options)
{
using (var entry = cache.CreateEntry(key))
{
if (options != null)
{
entry.SetOptions(options);
}
entry.Value = value;
}
return value;
}
对于测试,需要通过扩展方法模拟一条安全路径,以使其能够完成。在 Set
中,它还会调用缓存条目上的扩展方法,因此也必须满足这一点。这会很快变得复杂,所以我建议使用具体的实现
//...
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
//...
public Test GetSystemUnderTest() {
var services = new ServiceCollection();
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var memoryCache = serviceProvider.GetService<IMemoryCache>();
return new Test(memoryCache);
}
[Fact]
public void TestCache() {
//Arrange
var sut = GetSystemUnderTest();
//Act
sut.SetCache("key", "value");
//Assert
//...
}
所以现在您可以访问功能齐全的内存缓存。
我有一个类似的问题,但我想偶尔禁用缓存以进行调试,因为必须不断清除缓存很痛苦。自己 mock/fake 他们(使用 StructureMap
依赖注入)。
您也可以在测试中轻松使用它们。
public class DefaultRegistry: Registry
{
public static IConfiguration Configuration = new ConfigurationBuilder()
.SetBasePath(HttpRuntime.AppDomainAppPath)
.AddJsonFile("appsettings.json")
.Build();
public DefaultRegistry()
{
For<IConfiguration>().Use(() => Configuration);
#if DEBUG && DISABLE_CACHE <-- compiler directives
For<IMemoryCache>().Use(
() => new MemoryCacheFake()
).Singleton();
#else
var memoryCacheOptions = new MemoryCacheOptions();
For<IMemoryCache>().Use(
() => new MemoryCache(Options.Create(memoryCacheOptions))
).Singleton();
#endif
For<SKiNDbContext>().Use(() => new SKiNDbContextFactory().CreateDbContext(Configuration));
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
});
}
}
public class MemoryCacheFake : IMemoryCache
{
public ICacheEntry CreateEntry(object key)
{
return new CacheEntryFake { Key = key };
}
public void Dispose()
{
}
public void Remove(object key)
{
}
public bool TryGetValue(object key, out object value)
{
value = null;
return false;
}
}
public class CacheEntryFake : ICacheEntry
{
public object Key {get; set;}
public object Value { get; set; }
public DateTimeOffset? AbsoluteExpiration { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
public TimeSpan? SlidingExpiration { get; set; }
public IList<IChangeToken> ExpirationTokens { get; set; }
public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; set; }
public CacheItemPriority Priority { get; set; }
public long? Size { get; set; }
public void Dispose()
{
}
}
TLDR
向下滚动到代码片段以间接模拟缓存 setter(具有不同的到期时间 属性)
/TLDR
虽然确实不能使用 Moq 或大多数其他模拟框架 直接 模拟扩展方法,但通常可以间接模拟它们 - 这当然是围绕 IMemoryCache
建造的案例
正如我在 中指出的那样,从根本上说,所有扩展方法在其执行过程中的某处都会调用三种接口方法之一。
Nkosi's raises very valid points: it can get complicated very quickly and you can use a concrete implementation to test things. This is a perfectly valid approach to use. However, strictly speaking, if you go down this path, your tests will depend on the implementation of third party code. In theory, it's possible that changes to this will break your test(s) - in this situation, this is highly unlikely to happen because the caching 存储库已存档。
此外,使用具有大量依赖项的具体实现可能会涉及大量开销。如果您每次都创建一组干净的依赖项并且您有很多测试,这可能会给您的构建服务器增加相当大的负载(我并不是说这里就是这种情况,这取决于许多因素)
最后你失去了另一个好处:通过自己调查源代码来模拟正确的东西,你更有可能了解你正在使用的库是如何工作的。因此,您可能会学习如何更好地使用它,并且几乎肯定会学到其他东西。
对于您正在调用的扩展方法,您应该只需要三个带有回调的设置调用来断言调用参数。这可能不适合您,具体取决于您要测试的内容。
[Fact]
public void TestMethod()
{
var expectedKey = "expectedKey";
var expectedValue = "expectedValue";
var expectedMilliseconds = 100;
var mockCache = new Mock<IMemoryCache>();
var mockCacheEntry = new Mock<ICacheEntry>();
string? keyPayload = null;
mockCache
.Setup(mc => mc.CreateEntry(It.IsAny<object>()))
.Callback((object k) => keyPayload = (string)k)
.Returns(mockCacheEntry.Object); // this should address your null reference exception
object? valuePayload = null;
mockCacheEntry
.SetupSet(mce => mce.Value = It.IsAny<object>())
.Callback<object>(v => valuePayload = v);
TimeSpan? expirationPayload = null;
mockCacheEntry
.SetupSet(mce => mce.AbsoluteExpirationRelativeToNow = It.IsAny<TimeSpan?>())
.Callback<TimeSpan?>(dto => expirationPayload = dto);
// Act
var success = _target.SetCacheValue(expectedKey, expectedValue,
new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMilliseconds(expectedMilliseconds)));
// Assert
Assert.True(success);
Assert.Equal("key", keyPayload);
Assert.Equal("expectedValue", valuePayload as string);
Assert.Equal(expirationPayload, TimeSpan.FromMilliseconds(expectedMilliseconds));
}
这可以通过模拟 IMemoryCache 的 TryGetValue 方法而不是 Set 方法来完成(如前所述,这是一个扩展方法,因此不能被模拟)。
var mockMemoryCache = Substitute.For<IMemoryCache>();
mockMemoryCache.TryGetValue(Arg.Is<string>(x => x.Equals(key)), out string expectedValue)
.Returns(x =>
{
x[1] = value;
return true;
});
var converter = new sut(mockMemoryCache);
我在 .Net 5 项目中也遇到过这个问题,我通过包装内存缓存并仅公开我需要的功能来解决它。这样我就符合 ISP 并且更容易进行单元测试。
我创建了一个界面
public interface IMemoryCacheWrapper
{
bool TryGetValue<T>(string Key, out T cache);
void Set<T>(string key, T cache);
}
使用 MS 依赖项注入在我的包装器 class 中实现了内存缓存逻辑,因此我不依赖于我正在测试的 class 中的那些实现细节,而且它还有额外的好处遵守 SRP。
public class MemoryCacheWrapper : IMemoryCacheWrapper
{
private readonly IMemoryCache _memoryCache;
public MemoryCacheWrapper(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public void Set<T>(string key, T cache)
{
_memoryCache.Set(key, cache);
}
public bool TryGetValue<T>(string Key, out T cache)
{
if (_memoryCache.TryGetValue(Key, out T cachedItem))
{
cache = cachedItem;
return true;
}
cache = default(T);
return false;
}
}
我将内存缓存包装器添加到依赖项注入中,并用包装器替换了代码中的系统内存缓存,这就是我在测试中模拟的内容。总而言之,这是一个相对较快的工作,我认为结构也更好。
在我的测试中,我添加了这个,以便它模仿缓存更新。
_memoryCacheWrapperMock = new Mock<IMemoryCacheWrapper>();
_memoryCacheWrapperMock.Setup(s => s.Set(It.IsAny<string>(), It.IsAny<IEnumerable<IClientSettingsDto>>()))
.Callback<string, IEnumerable<IClientSettingsDto>>((key, cache) =>
{
_memoryCacheWrapperMock.Setup(s => s.TryGetValue(key, out cache))
.Returns(true);
});
public sealed class NullMemoryCache : IMemoryCache
{
public ICacheEntry CreateEntry(object key)
{
return new NullCacheEntry() { Key = key };
}
public void Dispose()
{
}
public void Remove(object key)
{
}
public bool TryGetValue(object key, out object value)
{
value = null;
return false;
}
private sealed class NullCacheEntry : ICacheEntry
{
public DateTimeOffset? AbsoluteExpiration { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
public IList<IChangeToken> ExpirationTokens { get; set; }
public object Key { get; set; }
public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; set; }
public CacheItemPriority Priority { get; set; }
public long? Size { get; set; }
public TimeSpan? SlidingExpiration { get; set; }
public object Value { get; set; }
public void Dispose()
{
}
}
}
我正在使用 asp net core 1.0 和 xunit。
我正在尝试为一些使用 IMemoryCache
的代码编写单元测试。但是,每当我尝试在 IMemoryCache
中设置一个值时,我都会收到 Null 引用错误。
我的单元测试代码是这样的:
IMemoryCache
被注入到我要测试的 class 中。但是,当我尝试在测试中的缓存中设置一个值时,我得到一个空引用。
public Test GetSystemUnderTest()
{
var mockCache = new Mock<IMemoryCache>();
return new Test(mockCache.Object);
}
[Fact]
public void TestCache()
{
var sut = GetSystemUnderTest();
sut.SetCache("key", "value"); //NULL Reference thrown here
}
这是 class 测试...
public class Test
{
private readonly IMemoryCache _memoryCache;
public Test(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public void SetCache(string key, string value)
{
_memoryCache.Set(key, value, new MemoryCacheEntryOptions {SlidingExpiration = TimeSpan.FromHours(1)});
}
}
我的问题是...我需要以某种方式设置 IMemoryCache
吗?为 DefaultValue 设置一个值?当 IMemoryCache
被 Mocked 时,默认值是多少?
IMemoryCache.Set
是一种扩展方法,因此不能使用 Moq 框架进行模拟。
扩展代码可用 here
public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options)
{
using (var entry = cache.CreateEntry(key))
{
if (options != null)
{
entry.SetOptions(options);
}
entry.Value = value;
}
return value;
}
对于测试,需要通过扩展方法模拟一条安全路径,以使其能够完成。在 Set
中,它还会调用缓存条目上的扩展方法,因此也必须满足这一点。这会很快变得复杂,所以我建议使用具体的实现
//...
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
//...
public Test GetSystemUnderTest() {
var services = new ServiceCollection();
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var memoryCache = serviceProvider.GetService<IMemoryCache>();
return new Test(memoryCache);
}
[Fact]
public void TestCache() {
//Arrange
var sut = GetSystemUnderTest();
//Act
sut.SetCache("key", "value");
//Assert
//...
}
所以现在您可以访问功能齐全的内存缓存。
我有一个类似的问题,但我想偶尔禁用缓存以进行调试,因为必须不断清除缓存很痛苦。自己 mock/fake 他们(使用 StructureMap
依赖注入)。
您也可以在测试中轻松使用它们。
public class DefaultRegistry: Registry
{
public static IConfiguration Configuration = new ConfigurationBuilder()
.SetBasePath(HttpRuntime.AppDomainAppPath)
.AddJsonFile("appsettings.json")
.Build();
public DefaultRegistry()
{
For<IConfiguration>().Use(() => Configuration);
#if DEBUG && DISABLE_CACHE <-- compiler directives
For<IMemoryCache>().Use(
() => new MemoryCacheFake()
).Singleton();
#else
var memoryCacheOptions = new MemoryCacheOptions();
For<IMemoryCache>().Use(
() => new MemoryCache(Options.Create(memoryCacheOptions))
).Singleton();
#endif
For<SKiNDbContext>().Use(() => new SKiNDbContextFactory().CreateDbContext(Configuration));
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
});
}
}
public class MemoryCacheFake : IMemoryCache
{
public ICacheEntry CreateEntry(object key)
{
return new CacheEntryFake { Key = key };
}
public void Dispose()
{
}
public void Remove(object key)
{
}
public bool TryGetValue(object key, out object value)
{
value = null;
return false;
}
}
public class CacheEntryFake : ICacheEntry
{
public object Key {get; set;}
public object Value { get; set; }
public DateTimeOffset? AbsoluteExpiration { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
public TimeSpan? SlidingExpiration { get; set; }
public IList<IChangeToken> ExpirationTokens { get; set; }
public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; set; }
public CacheItemPriority Priority { get; set; }
public long? Size { get; set; }
public void Dispose()
{
}
}
TLDR
向下滚动到代码片段以间接模拟缓存 setter(具有不同的到期时间 属性)
/TLDR
虽然确实不能使用 Moq 或大多数其他模拟框架 直接 模拟扩展方法,但通常可以间接模拟它们 - 这当然是围绕 IMemoryCache
正如我在
Nkosi's
此外,使用具有大量依赖项的具体实现可能会涉及大量开销。如果您每次都创建一组干净的依赖项并且您有很多测试,这可能会给您的构建服务器增加相当大的负载(我并不是说这里就是这种情况,这取决于许多因素)
最后你失去了另一个好处:通过自己调查源代码来模拟正确的东西,你更有可能了解你正在使用的库是如何工作的。因此,您可能会学习如何更好地使用它,并且几乎肯定会学到其他东西。
对于您正在调用的扩展方法,您应该只需要三个带有回调的设置调用来断言调用参数。这可能不适合您,具体取决于您要测试的内容。
[Fact]
public void TestMethod()
{
var expectedKey = "expectedKey";
var expectedValue = "expectedValue";
var expectedMilliseconds = 100;
var mockCache = new Mock<IMemoryCache>();
var mockCacheEntry = new Mock<ICacheEntry>();
string? keyPayload = null;
mockCache
.Setup(mc => mc.CreateEntry(It.IsAny<object>()))
.Callback((object k) => keyPayload = (string)k)
.Returns(mockCacheEntry.Object); // this should address your null reference exception
object? valuePayload = null;
mockCacheEntry
.SetupSet(mce => mce.Value = It.IsAny<object>())
.Callback<object>(v => valuePayload = v);
TimeSpan? expirationPayload = null;
mockCacheEntry
.SetupSet(mce => mce.AbsoluteExpirationRelativeToNow = It.IsAny<TimeSpan?>())
.Callback<TimeSpan?>(dto => expirationPayload = dto);
// Act
var success = _target.SetCacheValue(expectedKey, expectedValue,
new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMilliseconds(expectedMilliseconds)));
// Assert
Assert.True(success);
Assert.Equal("key", keyPayload);
Assert.Equal("expectedValue", valuePayload as string);
Assert.Equal(expirationPayload, TimeSpan.FromMilliseconds(expectedMilliseconds));
}
这可以通过模拟 IMemoryCache 的 TryGetValue 方法而不是 Set 方法来完成(如前所述,这是一个扩展方法,因此不能被模拟)。
var mockMemoryCache = Substitute.For<IMemoryCache>();
mockMemoryCache.TryGetValue(Arg.Is<string>(x => x.Equals(key)), out string expectedValue)
.Returns(x =>
{
x[1] = value;
return true;
});
var converter = new sut(mockMemoryCache);
我在 .Net 5 项目中也遇到过这个问题,我通过包装内存缓存并仅公开我需要的功能来解决它。这样我就符合 ISP 并且更容易进行单元测试。
我创建了一个界面
public interface IMemoryCacheWrapper
{
bool TryGetValue<T>(string Key, out T cache);
void Set<T>(string key, T cache);
}
使用 MS 依赖项注入在我的包装器 class 中实现了内存缓存逻辑,因此我不依赖于我正在测试的 class 中的那些实现细节,而且它还有额外的好处遵守 SRP。
public class MemoryCacheWrapper : IMemoryCacheWrapper
{
private readonly IMemoryCache _memoryCache;
public MemoryCacheWrapper(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public void Set<T>(string key, T cache)
{
_memoryCache.Set(key, cache);
}
public bool TryGetValue<T>(string Key, out T cache)
{
if (_memoryCache.TryGetValue(Key, out T cachedItem))
{
cache = cachedItem;
return true;
}
cache = default(T);
return false;
}
}
我将内存缓存包装器添加到依赖项注入中,并用包装器替换了代码中的系统内存缓存,这就是我在测试中模拟的内容。总而言之,这是一个相对较快的工作,我认为结构也更好。
在我的测试中,我添加了这个,以便它模仿缓存更新。
_memoryCacheWrapperMock = new Mock<IMemoryCacheWrapper>();
_memoryCacheWrapperMock.Setup(s => s.Set(It.IsAny<string>(), It.IsAny<IEnumerable<IClientSettingsDto>>()))
.Callback<string, IEnumerable<IClientSettingsDto>>((key, cache) =>
{
_memoryCacheWrapperMock.Setup(s => s.TryGetValue(key, out cache))
.Returns(true);
});
public sealed class NullMemoryCache : IMemoryCache
{
public ICacheEntry CreateEntry(object key)
{
return new NullCacheEntry() { Key = key };
}
public void Dispose()
{
}
public void Remove(object key)
{
}
public bool TryGetValue(object key, out object value)
{
value = null;
return false;
}
private sealed class NullCacheEntry : ICacheEntry
{
public DateTimeOffset? AbsoluteExpiration { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
public IList<IChangeToken> ExpirationTokens { get; set; }
public object Key { get; set; }
public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; set; }
public CacheItemPriority Priority { get; set; }
public long? Size { get; set; }
public TimeSpan? SlidingExpiration { get; set; }
public object Value { get; set; }
public void Dispose()
{
}
}
}