C# Юнит-тест: Конкурентные запросы с MemoryCache и SemaphoreSlim

Вопрос или проблема

Я провожу юнит-тестирование сервиса, который получает данные из API и кэширует их с помощью IMemoryCache и SemaphoreSlim для обработки конкурентных запросов. Я хочу смоделировать сценарий с несколькими конкурентными запросами, где:

  • Первый запрос обходит кэш, получает данные из API и
    наполняет кэш.
  • Последующие запросы получают данные из кэша и избегают
    избыточных вызовов API.

В своем тесте я использую реальный экземпляр MemoryCache вместо его имитации. Это хороший подход или есть лучшие способы достижения этого сценария в моем юнит-тесте? Буду признателен за любые предложения.

Вот мой юнит-тест

public async Task GetSigningKeysFromJwkAsync_ConcurrentRequests_OnlyOneFetchAndOthersUseCache()
{
    // Arrange
    var httpClient = HttpClient();
    var mockHttpClientFactory = new Mock<IHttpClientFactory>();
    mockHttpClientFactory.Setup(factory
        => factory.CreateClient("client")).Returns(httpClient);
    var memoryCache = new MemoryCache(new MemoryCacheOptions());  // Это вопрос
    var publicKeyService = new PublicKeyService(mockHttpClientFactory.Object, memoryCache);
    var bffUrl = new Uri("https://dummy-link/jwks");


    // Act
    var tasks = new List<Task<Dictionary<string, IEnumerable<SecurityKey>>>>();
    for (int i = 0; i < 10; i++) // Смоделировать 10 конкурентных запросов
    {
        tasks.Add(publicKeyService.GetSigningKeysFromJwkAsync(bffUrl));
    }
    await Task.WhenAll(tasks);

    // Assert

    var firstResult = tasks[0].Result;
    foreach (var task in tasks)
    {
        Assert.Equal(firstResult, task.Result);
    }
}

Вот мой метод, который я пытаюсь протестировать

public async Task<Dictionary<string, IEnumerable<SecurityKey>>?> GetSigningKeysFromJwkAsync(Uri url)
{
    if (_cache.TryGetValue(_cacheKey, out Dictionary<string, IEnumerable<SecurityKey>>? cachedKeys))
    {
        return cachedKeys;
    }

    await _semaphore.WaitAsync();

    try
    {
        if (_cache.TryGetValue(_cacheKey, out cachedKeys))
        {
            return cachedKeys;
        }

        HttpResponseMessage response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        var jwksJson = await response.Content.ReadAsStringAsync();

        var issuerKeys = JsonSerializer.Deserialize<Dictionary<string, string>>(jwksJson);

        var issuerSecurityKeySet = new Dictionary<string, IEnumerable<SecurityKey>>();
        if (issuerKeys != null && issuerKeys.Any())
        {
            foreach (var ik in issuerKeys)
            {
                var jwks = new JsonWebKeySet(ik.Value);
                issuerSecurityKeySet.Add(ik.Key, jwks.GetSigningKeys());
            }
        }

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromHours(1));
        _cache.Set(_cacheKey, issuerSecurityKeySet, cacheEntryOptions);

        return issuerSecurityKeySet;
    }
    finally
    {
        _semaphore.Release();
    }
} 

Ответ или решение

Тестирование C# Юнит: Параллельные Запросы с Использованием MemoryCache и SemaphoreSlim

В данном ответе мы рассмотрим, как правильно организовать юнит-тест для сервиса, который извлекает данные из API и использует IMemoryCache для кэширования. Ваша задача состоит в том, чтобы протестировать сценарий с несколькими параллельными запросами, при котором первый запрос должен обойти кэш, извлечь данные из API и заполнить кэш последующими запросами.

Основные компоненты теста

  1. Использование MemoryCache: Использование реального экземпляра MemoryCache вместо его мокирования может быть разумным подходом в данном случае, так как это позволит вам проверить реальное поведение кэширования. Это позволит убедиться в том, что проверки кэша и логика заполнения кэша работают корректно.

  2. SemaphoreSlim: Этот механизм синхронизации очень полезен, чтобы обеспечить, что только один поток может выполнить асинхронный вызов API одновременно, в то время как другие потоки будут ожидать своей очереди.

  3. Параллельные запросы: Создание списка задач (tasks), которые будут выполняться одновременно, эмулирует поведение множества пользователей, обращающихся к вашему сервису.

Пример юнит-теста

Вот ваш юнит-тест, который выполняет описанные выше действия:

public async Task GetSigningKeysFromJwkAsync_ConcurrentRequests_OnlyOneFetchAndOthersUseCache()
{
    // Arrange
    var httpClient = HttpClient(); // Настройте HttpClient для имитации API
    var mockHttpClientFactory = new Mock<IHttpClientFactory>();
    mockHttpClientFactory.Setup(factory => factory.CreateClient("client")).Returns(httpClient);

    var memoryCache = new MemoryCache(new MemoryCacheOptions());
    var publicKeyService = new PublicKeyService(mockHttpClientFactory.Object, memoryCache);
    var bffUrl = new Uri("https://dummy-link/jwks");

    // Act
    var tasks = new List<Task<Dictionary<string, IEnumerable<SecurityKey>>>>();
    for (int i = 0; i < 10; i++) // Эмуляция 10 параллельных запросов
    {
        tasks.Add(publicKeyService.GetSigningKeysFromJwkAsync(bffUrl));
    }
    await Task.WhenAll(tasks);

    // Assert
    var firstResult = tasks[0].Result;
    foreach (var task in tasks)
    {
        Assert.Equal(firstResult, task.Result);
    }
}

Логика метода для тестирования

Метод GetSigningKeysFromJwkAsync выполняет следующие шаги:

  1. Проверяет наличие данных в кэше.
  2. Если данных нет, захватывает семафор для гарантии, что только один поток выполняет API-вызов.
  3. Пытается заново проверить наличие данных в кэше после получения семафора.
  4. Если всё равно данных нет, делает HTTP-запрос к API.
  5. Кэширует данные и возвращает результат.

Рекомендации по улучшению теста

  1. Используйте мок-объекты для HttpClient: Вместо запуска реального HTTP-запроса в тестах может быть полезно использовать мок-объекты. Это позволит избежать проблем производительности и гарантировать, что ваши тесты будут стабильны.

  2. Добавьте проверки на время выполнения: Проверьте, что время выполнения API-вызова для последующих запросов укладывается в допустимые рамки и не превышает ожидания.

  3. Тестируйте и на ошибки: Убедитесь, что ваш код правильно обрабатывает ошибки, такие как ошибки сетевого соединения или недоступность API, и убедитесь, что тесты полностью это покрывают.

  4. Логгирование и диагностика: Рассмотрите возможность добавления логгирования в асинхронные методы для лучшего понимания блужданий в коде, особенно для таких сценариев, как конкуренция.

Заключение

Ваш текущий подход к тестированию с использованием реального MemoryCache является целесообразным, так как он дает представление о реальном поведении кэша. Применяя рекомендации выше, вы сможете улучшить свои тесты и обеспечить надежностью и стабильность вашего кода.

Оцените материал
Добавить комментарий

Капча загружается...