Mock IMemoryCache с исключением метаданных

Я пытаюсь высмеять IMemoryCache с помощью Moq. Я получаю эту ошибку:

Исключение типа "System.NotSupportedException" произошло в Moq.dll, но не был обработан в коде пользователя

Дополнительная информация: выражение ссылается на метод, который не принадлежат обманутому объекту: x = > x.Get <String> (It.IsAny <String> ())

Мой издевательский код:

namespace Iag.Services.SupplierApiTests.Mocks
{
    public static class MockMemoryCacheService
    {
        public static IMemoryCache GetMemoryCache()
        {
            Mock<IMemoryCache> mockMemoryCache = new Mock<IMemoryCache>();
            mockMemoryCache.Setup(x => x.Get<string>(It.IsAny<string>())).Returns("");<---------- **ERROR**
            return mockMemoryCache.Object;
        }
    }
}

Почему я получаю эту ошибку?

Это проверенный код:

var cachedResponse = _memoryCache.Get<String>(url);

Где _memoryCache имеет тип IMemoryCache

Как я высмеиваю _memoryCache.Get<String>(url) выше и пусть он возвращает null?

Изменить. Как бы я сделал то же самое, но для _memoryCache.Set<String>(url, response);? Я не против, что он возвращает, мне просто нужно добавить метод к макету, чтобы он не бросался, когда он вызывается.

Идя по ответу на этот вопрос, я пробовал:

mockMemoryCache
    .Setup(m => m.CreateEntry(It.IsAny<object>())).Returns(null as ICacheEntry);

Потому что в расширениях memoryCache он показывает, что он использует CreateEntry внутри Set. Но это ошибка из-за того, что "ссылка на объект не установлена ​​на экземпляр объекта".

Ответ 1

Согласно исходному коду для MemoryCacheExtensions.cs,

Метод расширения Get<TItem> использует следующее

public static TItem Get<TItem>(this IMemoryCache cache, object key) {
    TItem value;
    cache.TryGetValue<TItem>(key, out value);
    return value;
}

public static bool TryGetValue<TItem>(this IMemoryCache cache, object key, out TItem value) {
    object result;
    if (cache.TryGetValue(key, out result)) {
        value = (TItem)result;
        return true;
    }

    value = default(TItem);
    return false;
}

Обратите внимание, что по сути он использует метод TryGetValue(Object, out Object).

Учитывая, что с Moq невозможно смоделировать методы расширения, попробуйте смоделировать элементы интерфейса, к которым обращаются методы расширения.

Ссылаясь на MockMemoryCacheService Moq, обновите MockMemoryCacheService чтобы правильно настроить метод TryGetValue для теста.

public static class MockMemoryCacheService {
    public static IMemoryCache GetMemoryCache(object expectedValue) {
        var mockMemoryCache = new Mock<IMemoryCache>();
        mockMemoryCache
            .Setup(x => x.TryGetValue(It.IsAny<object>(), out expectedValue))
            .Returns(true);
        return mockMemoryCache.Object;
    }
}

Из комментариев

Обратите внимание, что при TryGetValue (вместо Get) параметр out должен быть объявлен как object даже если это не так.

Например:

int expectedNumber = 1; 
object expectedValue = expectedNumber. 

Если вы этого не сделаете, он будет соответствовать шаблонному методу расширения с тем же именем.

Вот пример использования модифицированного сервиса о том, как memoryCache.Get<String>(url) и дать ему вернуть null

[TestMethod]
public void _IMemoryCacheTestWithMoq() {
    var url = "fakeURL";
    object expected = null;

    var memoryCache = MockMemoryCacheService.GetMemoryCache(expected);

    var cachedResponse = memoryCache.Get<string>(url);

    Assert.IsNull(cachedResponse);
    Assert.AreEqual(expected, cachedResponse);
}

ОБНОВИТЬ

Тот же процесс может быть применен для метода расширения Set<> который выглядит следующим образом.

public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value) {
    var entry = cache.CreateEntry(key);
    entry.Value = value;
    entry.Dispose();

    return value;
}

Этот метод использует метод CreateEntry который возвращает ICacheEntry которым также ICacheEntry. Так что настройте макет так, чтобы он возвращал ложную запись, как в следующем примере

[TestMethod]
public void _IMemoryCache_Set_With_Moq() {
    var url = "fakeURL";
    var response = "json string";

    var memoryCache = Mock.Of<IMemoryCache>();
    var cachEntry = Mock.Of<ICacheEntry>();

    var mockMemoryCache = Mock.Get(memoryCache);
    mockMemoryCache
        .Setup(m => m.CreateEntry(It.IsAny<object>()))
        .Returns(cachEntry);

    var cachedResponse = memoryCache.Set<string>(url, response);

    Assert.IsNotNull(cachedResponse);
    Assert.AreEqual(response, cachedResponse);
}

Ответ 2

Если вы вызываете Set с помощью MemoryCacheEntryOptions and.AddExpirationToken, то вам также понадобится запись, чтобы иметь список токенов.

Это дополнение к ответу @Nkosi выше. Пример:

// cache by filename: https://jalukadev.blogspot.com/2017/06/cache-dependency-in-aspnet-core.html
var fileInfo = new FileInfo(filePath);
var fileProvider = new PhysicalFileProvider(fileInfo.DirectoryName);
var options = new MemoryCacheEntryOptions();
options.AddExpirationToken(fileProvider.Watch(fileInfo.Name));
this.memoryCache.Set(key, cacheValue, options);

Макет должен включать в себя:

// https://github.com/aspnet/Caching/blob/45d42c26b75c2436f2e51f4af755c9ec58f62deb/src/Microsoft.Extensions.Caching.Memory/CacheEntry.cs
var cachEntry = Mock.Of<ICacheEntry>();
Mock.Get(cachEntry).SetupGet(c => c.ExpirationTokens).Returns(new List<IChangeToken>());

var mockMemoryCache = Mock.Get(memoryCache);
mockMemoryCache
    .Setup(m => m.CreateEntry(It.IsAny<object>()))
    .Returns(cachEntry);

Ответ 3

В интерфейсе Microsoft.Extensions.Caching.Memory.IMemoryCache нет метода Microsoft.Extensions.Caching.Memory.IMemoryCache.Get(object). Тот, который вы пытаетесь использовать, находится в Microsoft.Extensions.Caching.Memory.CacheExtensions. Вы можете посмотреть на эти ответы, чтобы напрямую ответить на ваш вопрос;

Как использовать Moq для издевательства над методом расширения?

Спокойные методы расширения с помощью Moq.

Вы также должны знать, как настроить Moq для последующих использования;

В вашем коде указано, что метод Get возвращает строку и принимает строковый параметр. Если ваша тестовая конфигурация следует за всем тестом, это нормально. Но по объявлению метод Get принимает объект как ключ. Таким образом, ваш код для предиката параметра будет It.IsAny<object>().

Во-вторых, если вы хотите вернуть значение null, вы должны указать его на тип, который фактически возвращает ваша функция (например, .Returns((string)null)). Это связано с тем, что существуют другие перегрузки для Moq.Language.IReturns.Returns, и компилятор не может решить, к какому из них вы пытаетесь обратиться.