繁体   English   中英

在单元测试中模拟 IMemoryCache

[英]Mock IMemoryCache in unit test

我正在使用 asp.net 核心 1.0 和 xunit。

我正在尝试为一些使用IMemoryCache的代码编写单元测试。 但是,每当我尝试在IMemoryCache中设置一个值时,我都会收到 Null 引用错误。

我的单元测试代码是这样的:
IMemoryCache注入到我要测试的class中。 但是,当我尝试在测试中的缓存中设置一个值时,我得到了一个 null 引用。

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被模拟时,默认值是什么?

IMemoryCache.Set是一个扩展方法,因此不能使用Moq框架IMemoryCache.Set

扩展的代码可以在这里找到

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
    //...
}

所以现在您可以访问功能齐全的内存缓存。

TLDR

向下滚动到代码片段以间接模拟缓存设置器(具有不同的到期属性)

/TLDR

虽然确实不能使用Moq或大多数其他模拟框架直接模拟扩展方法,但它们通常可以间接IMemoryCache对于那些围绕IMemoryCache构建的方法来说肯定是这种情况

正如我在这个答案中指出的那样,从根本上说,所有扩展方法都会在其执行过程中的某处调用三个接口方法之一。

Nkosi 的回答提出了非常有效的观点:它很快就会变得复杂,您可以使用具体的实现来测试事物。 这是一种完全有效的使用方法。 然而,严格来说,如果你沿着这条路走下去,你的测试将取决于第三方代码的实现。 从理论上讲,对此的更改可能会破坏您的测试 - 在这种情况下,这种情况极不可能发生,因为缓存存储库已存档。

此外,使用具有大量依赖项的具体实现可能会涉及大量开销。 如果您每次都创建一组干净的依赖项并且您有许多测试,这可能会给您的构建服务器增加相当大的负载(我不是说这里就是这种情况,这将取决于许多因素)

最后你失去了另一个好处:通过自己研究源代码来模拟正确的东西,你更有可能了解你正在使用的库是如何工作的。 因此,您可能会学习如何更好地使用它,并且几乎肯定会学到其他东西。

对于您正在调用的扩展方法,您应该只需要三个带有回调的设置调用来断言调用参数。 这可能不适合您,具体取决于您要测试的内容。

[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));
}
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()
        {
            
        }
    }
}

我有一个类似的问题,但我想偶尔禁用缓存进行调试,因为它必须清除缓存很痛苦。 只需自己模拟/伪造它们(使用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()
    {

    }
}

我也在 .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);
            });

这可以通过模拟 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);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM