Как использовать Moq для создания макета класса StackExchange.Redis ConnectionMultiplexer?

Я работаю над макетом поведения, связанного с библиотекой StackExchange.Redis, но не могу понять, как правильно имитировать закрытые классы, которые он использует. Конкретный пример в моем вызывающем коде, я делаю что-то вроде этого:

    var cachable = command as IRedisCacheable;

    if (_cache.Multiplexer.IsConnected == false)
    {
        _logger.Debug("Not using the cache because the connection is not available");

        cacheAvailable = false;
    }
    else if (cachable == null)
    {

Ключевая строка там - _cache.Multiplexer.IsConnected, где я проверяю, чтобы убедиться, что у меня есть действующее соединение, прежде чем использовать кеш. Поэтому в своих тестах я хочу смоделировать это поведение примерно так:

    _mockCache = new Mock<IDatabase>();
    _mockCache.Setup(cache => cache.Multiplexer.IsConnected).Returns(false);

Однако, хотя этот код компилируется нормально, я получаю эту ошибку при запуске теста:

Исключение Moq, выброшенное в ходе теста

Я также попытался издеваться над самим классом мультиплексора и предоставить это своему фиктивному кешу, но я столкнулся с тем фактом, что класс мультиплексора запечатан:

    _mockCache = new Mock<IDatabase>();
    var mockMultiplexer = new Mock<ConnectionMultiplexer>();
    mockMultiplexer.Setup(c => c.IsConnected).Returns(false);
    _mockCache.Setup(cache => cache.Multiplexer).Returns(mockMultiplexer.Object);

... но это приводит к этой ошибке:

Ошибка при попытке имитации запечатанного класса

В конечном итоге я хочу контролировать, является ли это свойство истинным или ложным в моих тестах, так есть ли правильный способ смоделировать что-то вроде этого?


person Sam Storie    schedule 04.02.2015    source источник


Ответы (3)


На мой взгляд, лучший подход - заключить все ваше взаимодействие с Redis в собственный класс и интерфейс. Что-то вроде CacheHandler : ICacheHandler и ICacheHandler. Весь ваш код будет разговаривать только с ICacheHandler.

Таким образом, вы устраняете жесткую зависимость от Redis (вы можете поменять местами реализацию ICacheHandler по своему усмотрению). Вы также можете имитировать любое взаимодействие с вашим слоем кеширования, потому что оно запрограммировано против интерфейса.

Не следует тестировать StackExchange.Redis напрямую - это не код, который вы написали.

person Haney    schedule 04.02.2015
comment
Да, я все больше и больше чувствую, что мне нужен именно такой подход. Я хотел избежать обертывания чего-то вроде их интерфейса IDatabase или интерфейса, предоставляемого библиотекой ServiceStack.Redis, но на самом деле мне пока не нужны все эти функции. - person Sam Storie; 04.02.2015
comment
Хотя я согласен с добавлением класса-оболочки, разве этот подход не перемещает проблему обратно на уровень? Как мне написать модульный тест для моего класса-оболочки (ICacheHandler), который вызывает методы redis, если я не могу смоделировать его поведение? - person Necoras; 20.10.2016
comment
Тесты для реализаций ICacheHandler будут интеграционными, а не модульными. - person frostymarvelous; 30.03.2017

Вместо этого используйте интерфейс IConnectionMultiplexer. конкретного класса ConnectionMultiplexer в вашем собственном классе.

public interface ICacheable
{
   void DoYourJob();
}

public sealed class RedisCacheHandler : ICacheable
{
    private readonly IConnectionMultiplexer multiplexer;

    public RedisCacheHandler(IConnectionMultiplexer multiplexer)
    {
        this.multiplexer = multiplexer;
    }

    public void DoYourJob() 
    {
        var database = multiplexer.GetDatabase(1);

        // your code        
    }
}

Тогда вы можете легко поиздеваться и протестировать это:

// Arrange
var mockMultiplexer = new Mock<IConnectionMultiplexer>();

mockMultiplexer.Setup(_ => _.IsConnected).Returns(false);

var mockDatabase = new Mock<IDatabase>();

mockMultiplexer
    .Setup(_ => _.GetDatabase(It.IsAny<int>(), It.IsAny<object>()))
    .Returns(mockDatabase.Object);

var cacheHandler = new RedisCacheHandler(mockMultiplexer.Object);

// Act
cacheHandler.DoYourJob();


// Assert
// your tests
person Andriy Tolstoy    schedule 30.10.2017

В приведенный выше ответ не включена более подробная настройка экземпляра mockDatabase. Я немного изо всех сил пытался найти рабочий пример чего-то столь же простого, как издевательство над методом IDatabase StringGet (например, обработка необязательных параметров, использование RedisKey vs string, использование RedisValue vs string и т. Д.), Поэтому подумал, что поделюсь. Вот что у меня сработало.

Эта тестовая установка:

var expected = "blah";
RedisValue expectedValue = expected;

mockDatabase.Setup(db => db.StringGet(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.Returns(expectedValue);

Чтобы повлиять на то, что возвращается этим протестированным вызовом метода:

var redisValue = _connectionMultiplexer.GetDatabase().StringGet(key);
person Dan Sabin    schedule 09.08.2018