我正在使用asp net core 1.0和xunit。
我正在尝试为使用IMemoryCache
的一些代码编写单元测试。但是,每当我尝试在IMemoryCache
中设置一个值时,我都会得到一个Null参考错误。
我的单位测试代码是这样的:
IMemoryCache
被注入我想要测试的类中。但是,当我尝试在测试中设置缓存中的值时,我得到一个空引用。
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
}
这是课程测试...
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
被模拟时,默认值是什么?
答案 0 :(得分:11)
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
//...
}
现在您可以访问功能完整的内存缓存。
答案 1 :(得分:3)
向下滚动到代码段以间接模拟缓存设置器(具有不同的expiry属性)
虽然确实不能使用Moq或大多数其他模拟框架直接 模拟扩展方法,但通常可以间接模拟这些扩展方法-对于那些内置的扩展程序,确实如此在IMemoryCache
正如我在this answer中所指出的,从根本上讲,所有扩展方法都在执行过程中的某个位置调用这三个接口方法之一。
Nkosi's answer提出了非常有效的观点:它会很快变得复杂,您可以使用具体的实现来测试事物。这是一种完全有效的使用方法。但是,严格来说,如果您走这条路,您的测试将取决于第三方代码的实现。从理论上讲,对此所做的更改有可能破坏您的测试-在这种情况下,由于caching存储库已被存档,因此这种情况极不可能发生。
此外,有可能使用带有一堆依赖项的具体实现可能会涉及很多开销。如果您每次都创建一组干净的依赖项并且您进行了许多测试,那么这可能会给构建服务器增加相当大的负担(我并不是说这里就是这种情况,这取决于许多因素)>
最后,您失去了另一个好处:通过自己调查源代码以模拟正确的事物,您更有可能了解所使用的库的工作方式。因此,您可能会学习如何更好地使用它,并且几乎可以肯定会学习其他东西。
对于正在调用的扩展方法,您只需要三个带有回调的setup调用即可对调用参数进行断言。这可能不适合您,具体取决于您要测试的内容。
[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));
}
答案 2 :(得分:1)
我有一个类似的问题,但我想偶尔禁用缓存进行调试,因为它不得不清除缓存。只需自己模拟/伪造它们(使用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()
{
}
}
答案 3 :(得分:0)
这可以通过模拟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);
答案 4 :(得分:0)
我也在 .Net 5 项目中遇到了这个问题,我通过包装内存缓存并只公开我需要的功能来解决它。这样我就符合 ISP 并且更容易处理我的单元测试。
我创建了一个界面
public interface IMemoryCacheWrapper
{
bool TryGetValue<T>(string Key, out T cache);
void Set<T>(string key, T cache);
}
在我的包装类中实现了内存缓存逻辑,使用 MS 依赖注入,所以我不依赖于我被测类中的那些实现细节,而且它具有遵守 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);
});