web-dev-qa-db-ja.com

単体テストでIMemoryCacheをモックする

私はasp net core 1.0とxunitを使用しています。

IMemoryCacheを使用する一部のコードの単体テストを作成しようとしています。ただし、IMemoryCacheに値を設定しようとすると、Null参照エラーが発生します。

私のユニットテストコードは次のとおりです:
テストするクラスにIMemoryCacheが挿入されています。ただし、テストでキャッシュに値を設定しようとすると、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
}

そして、これはクラスのテストです...

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の場合、デフォルト値は何ですか?

19
Bill Posters

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

これで、完全に機能するメモリキャッシュにアクセスできるようになりました。

15
Nkosi

TLDR

コードスニペットまでスクロールして、キャッシュセッターを間接的にモックします(別の有効期限プロパティを使用)。

/ TLDR

Moq または他のほとんどのモックフレームワークを使用して、拡張メソッドを直接モックできないことは事実ですが、多くの場合、モックすることができます間接的に-そしてこれは確かにIMemoryCacheを中心に構築されたものの場合です

この答え で指摘したように、基本的に、すべての拡張メソッドは3つのインターフェイスメソッドのいずれかを実行中に呼び出します。

Nkosi'sanswer 非常に有効なポイントが発生します。非常に複雑になりやすく、具体的な実装を使用して物事をテストできます。これは完全に有効な使用方法です。ただし、厳密に言うと、このパスをたどると、テストはサードパーティのコードの実装に依存します。理論的には、これを変更するとテストが失敗する可能性があります。この状況では、 caching リポジトリがアーカイブされているため、これが発生する可能性はほとんどありません。

さらに、多数の依存関係を持つ具体的な実装を使用すると、多くのオーバーヘッドが発生する可能性があります。毎回、依存関係のクリーンなセットを作成していて、多くのテストを行うと、ビルドサーバーにかなりの負荷がかかる可能性があります(ここではそうではありません。多くの要因に依存します)。

最後に、もう1つの利点が失われます。適切なものを模倣するためにソースコードを自分で調査することにより、使用しているライブラリのしくみについて学ぶ可能性が高くなります。その結果、あなたはそれをよりよく使う方法を学ぶかもしれません、そして、あなたはほとんど確実に他のことを学ぶでしょう。

呼び出す拡張メソッドの場合、呼び出し引数でアサートするコールバックを含む3つのセットアップ呼び出しのみが必要です。テストする対象によっては、これは適切でない場合があります。

[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
user1007074

同様の問題がありましたが、キャッシュをクリアし続けなければならないので、デバッグのためにキャッシュを無効にしたい場合があります。 (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()
    {

    }
}
1