Как написать модульный тест для обработчика MediatR C#

#c# #unit-testing #moq #mediatr

Вопрос:

Я пытаюсь написать модульный тест с использованием фреймворка Moq для одного из моих обработчиков mediatrader. У меня есть следующий код. Вкратце, этот обработчик выполняет запрос для данного ключа и возвращает ответ, содержащий ключ и его значение, используя ядро EF. Также существует базовый механизм кэширования. Если ключ найден в кэше, он извлекается из кэша и возвращается.

Обработчик

 public GetConfigByKeyRequestHandler(MyContext context, ICacheProvider cacheProvider, IOptions<CacheConfigs> cacheConfigs)
{
    this.context = context;
    this.cacheProvider = cacheProvider;
    this.cacheConfigs = cacheConfigs?.Value;
}


public async Task<ConfigResponse> Handle(GetConfigByKeyRequest request, CancellationToken cancellationToken)
{
    ConfigResponse config;
    if (!await cacheProvider.ExistsAsync(request.Key))
    {
        config = await context.Configs
            .Where(x.ConfigKey.Equals(request.Key))
            .Select(x => 
                    new ConfigResponse {
                        ConfigKey = x.ConfigKey,
                        ConfigValue = x.ConfigValue 
                    })
            .FirstOrDefaultAsync(cancellationToken);

        if (config is not null)
        {
            await cacheProvider.PutAsync(new CacheItem<ConfigResponse>(request.Key, config), new CacheOptions
            {
                ExpireAfter = TimeSpan.FromMinutes(cacheConfigs.ExpireAfterInMinutes).TotalMilliseconds,
                ExpireInactive = TimeSpan.FromMinutes(cacheConfigs.ExpireInActiveInMinutes).TotalMilliseconds
            }); 
        }
        return config;
    }

    config = await cacheProvider.PullAsync<ConfigResponse>(request.Key);
    return config;
}

 

Я подумал, что мне следует рассмотреть 2 различных сценария:

  1. Когда ключ найден в кэше
  2. Когда ключ не найден в кэше и он возвращен из DbContext.

Модульные тесты

 private Mock<ICacheProvider> cacheProviderMock;
private IOptions<CacheConfigs> cacheConfigs;
public GetConfigByKeyRequestHandlerTests()
{
    cacheProviderMock = new Mock<ICacheProvider>();
    cacheConfigs = Options.Create(
        new CacheConfigs
        {
            ExpireAfterInMinutes = 3,
            ExpireInActiveInMinutes = 3
        });
}

[Fact]
public async Task GetConfigByKeyHandler_WhenKeyIsCached_ShouldReturnConfigByKey()
{
    // arrange 
    var options = new DbContextOptionsBuilder<MyContext>().UseInMemoryDatabase("MyInMemoryDatabase").Options;
    var configItems = Enumerable.Range(0, 5).Select(x => new Config
    {
        ConfigKey = $"key{x}",
        ConfigValue = $"value{x}"
    });

    using (var context = new MyContext(options))
    {
        await context.Configs.AddRangeAsync(configItems);
        await context.SaveChangesAsync();
    }

   
    using (var context = new MyContext(options))
    {
        cacheProviderMock.Setup(x => x.ExistsAsync(It.IsAny<string>())).Returns(Task.FromResult(true));
        cacheProviderMock.Setup(x => x.PullAsync<ConfigResponse>("key2"))
            .Returns(Task.FromResult(new ConfigResponse
            {
                ConfigKey = "key2",
                ConfigValue = "value2"
            }));
        var getConfigByKeyHandler = new GetConfigByKeyRequestHandler(context, cacheProviderMock.Object, cacheConfigs);
        var getConfigByKeyRequest = new GetConfigByKeyRequest("key2");
        // act
        var result = await getConfigByKeyHandler.Handle(getConfigByKeyRequest, CancellationToken.None);

        // assert
        Assert.NotNull(result);
        Assert.Equal("key2", result.ConfigKey);
    }

}
...
...
 

С той же логикой у меня есть еще один тест для другого сценария, когда ключ не кэшируется

 ...
...

[Fact]
public async Task GetConfigByKeyHandler_WhenKeyIsNotCached_ShouldReturnConfigByKey()
{
    // arrange 
    var options = new DbContextOptionsBuilder<MyContext>().UseInMemoryDatabase("MyInMemoryDatabase").Options;
    var configItems = Enumerable.Range(0, 5).Select(x => new Config
    {
        ConfigKey = $"key{x}",
        ConfigValue = $"value{x}"
    });

    using (var context = new MyContext(options))
    {
        await context.Configs.AddRangeAsync(configItems);
        await context.SaveChangesAsync();
    }

    
    using (var context = new MyContext(options))
    {
        cacheProviderMock.Setup(x => x.ExistsAsync(It.IsAny<string>())).Returns(Task.FromResult(false));
        var getConfigByKeyHandler = new GetConfigByKeyRequestHandler(context, cacheProviderMock.Object, cacheConfigs);
        var getConfigByKeyRequest = new GetConfigByKeyRequest("key2");
        // act
        var result = await getConfigByKeyHandler.Handle(getConfigByKeyRequest, CancellationToken.None);

        // assert
        Assert.NotNull(result);
        Assert.Equal("key2", result.ConfigKey);
    }


}
 

Я написал 2 модульных теста, которые охватывают сценарии, о которых я упоминал выше, но я не уверен, что они являются разумными, и я не уверен, что их следует тестировать таким образом. У вас есть какие-либо предложения о том, что/как мне следует писать тесты для обработчика, которым я поделился выше?

Комментарии:

1. Я думаю, поскольку у вас есть рабочий код, вы можете найти codereview.stackexchange.com лучший сайт для его улучшения. Вы, скорее всего, получите такие мнения, как 1. Поскольку вы на самом деле не издеваетесь над DbContext, у вас есть интеграционный тест, а не модульный тест, 2. ВЫСУШИТЕ настройки UseInMemoryDatabase 3. MediatR может быть излишним для «обработчиков запросов» и 4. Рассмотрите возможность использования шаблона «Кэш в сторону «, чтобы высушить ваши ветви управления императивным кэшем. Но, конечно, все это всего лишь мнения.

2. Эй. У меня была такая же ситуация в моем проекте. Я решил разделить логику на интеграционные тесты и модульные тесты. Я проверил всю MediatR логику с помощью интеграционных тестов. Модульные тесты помогли мне протестировать логику внутри обработчиков. В результате вы можете объединить два подхода тестов для достижения лучших результатов 🙂