Вопрос или проблема
Я провожу юнит-тестирование сервиса, который получает данные из 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 и заполнить кэш последующими запросами.
Основные компоненты теста
-
Использование
MemoryCache
: Использование реального экземпляра MemoryCache вместо его мокирования может быть разумным подходом в данном случае, так как это позволит вам проверить реальное поведение кэширования. Это позволит убедиться в том, что проверки кэша и логика заполнения кэша работают корректно. -
SemaphoreSlim: Этот механизм синхронизации очень полезен, чтобы обеспечить, что только один поток может выполнить асинхронный вызов API одновременно, в то время как другие потоки будут ожидать своей очереди.
-
Параллельные запросы: Создание списка задач (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
выполняет следующие шаги:
- Проверяет наличие данных в кэше.
- Если данных нет, захватывает семафор для гарантии, что только один поток выполняет API-вызов.
- Пытается заново проверить наличие данных в кэше после получения семафора.
- Если всё равно данных нет, делает HTTP-запрос к API.
- Кэширует данные и возвращает результат.
Рекомендации по улучшению теста
-
Используйте мок-объекты для HttpClient: Вместо запуска реального HTTP-запроса в тестах может быть полезно использовать мок-объекты. Это позволит избежать проблем производительности и гарантировать, что ваши тесты будут стабильны.
-
Добавьте проверки на время выполнения: Проверьте, что время выполнения API-вызова для последующих запросов укладывается в допустимые рамки и не превышает ожидания.
-
Тестируйте и на ошибки: Убедитесь, что ваш код правильно обрабатывает ошибки, такие как ошибки сетевого соединения или недоступность API, и убедитесь, что тесты полностью это покрывают.
-
Логгирование и диагностика: Рассмотрите возможность добавления логгирования в асинхронные методы для лучшего понимания блужданий в коде, особенно для таких сценариев, как конкуренция.
Заключение
Ваш текущий подход к тестированию с использованием реального MemoryCache является целесообразным, так как он дает представление о реальном поведении кэша. Применяя рекомендации выше, вы сможете улучшить свои тесты и обеспечить надежностью и стабильность вашего кода.