C#: использование Unit test с Apache Ignite

от автора

Итак, повторю, работающий тест – это минимальный критерий работающего кода. Мы в некоторых сервисах в качестве кэша используем Apache Ignite, и вот как раз эта часть кода с кэшированием не была покрыта тестами. Ниже опишу, как я справлялся с задачей, но, сначала сделаю пару ремарок + дам пару определений.

1) Цель статьи – показать, как можно решить проблему при написании юнит тестов, когда в коде есть зависимости от внешнего ресурса со статическими методами.

  • Unit тестирование – процесс в программировании, позволяет проверить бизнес-логику исходного кода, работает в оперативной памяти и не взаимодействует с внешними источниками (БД, файловая система, сеть и т.д.).
    Для запуска Unit тестов существует множество инструментов; лично я использую xUnit, AutoFixture, Moq.

  • xUnit – это технология для модульного тестирования;

  • AutoFixture упрощает инициализацию тестовых данных;

  • Moq предназначен для имитации объектов или для создания так называемых фейковых объектов. 

  • Статический метод не принадлежит объекту, он — часть класса и поэтому не может быть переопределен.

2) В данной статье приведен способ покрытия одного метода, но таким же образом можно покрыть и другие методы Apache Ignite. А также описанный подход подойдет для использования и с другими системами, где могут быть проблемы с покрытием кода тестами.  

  • Apache Ignite – это распределенная система управления базами данных для высокопроизводительных вычислений. Мы используем ее для кеширования данных.

Погнали!

Пример первоначального репозитория:

/// <summary> /// Изначальный репозиторий. /// </summary> public class DemoRepository {     /// <summary>     /// Клиент Ignite.     /// </summary>     private readonly IIgniteClient _igniteClient;      public DemoRepository(IIgniteClient igniteClient)     {         _igniteClient = igniteClient;     }      public IList<DemoModel> Get(         DemoFilter filter     )     {         // Получаем существующий кэш с указанным именем или создаем новый, используя конфигурацию шаблона.         var cache = _igniteClient.GetOrCreateCache<int, DemoModel>(nameof(DemoModel));          var cacheData = cache             // Получаем данные из кэша.             // Результирующий запрос будет преобразован в запрос SQL кэша.              .AsCacheQueryable()             .Where(                 item =>                     // Фильтруем данные по Id.                     filter.Ids == null || filter.Ids.Contains(item.Value.Id)             )             // Сортируем.             .OrderBy(item => item.Key)             // Используем пейджинг: Skip - сколько данных пропустить, Take - количество получаемых данных.             .Skip(filter.PageSize * filter.PageIndex)             .Take(filter.PageSize);          // Приводим к списку IList<DemoModel>.         return cacheData             .Select(item => item.Value)             .ToList();     } } 

Пример моделей:

/// <summary> /// Пример модели. /// </summary> public class DemoModel {     public int Id { get; }      public string Name { get; }      public DemoModel(int id, string name)     {         Id = id;         Name = name;     } } /// <summary> /// Пример фильтра. /// </summary> public class DemoFilter {     /// <summary>     /// Фильтр по Id.     /// </summary>     public IList<int> Ids { get; }      /// <summary>     /// Номер страницы получаемых данных.     /// </summary>     public int PageIndex { get; }      /// <summary>     /// Количество получаемых данных.     /// </summary>     public int PageSize { get; }      public DemoFilter(IList<int> ids, int pageIndex, int pageSize)     {         Ids = ids;         PageIndex = pageIndex;         PageSize = pageSize;     } } 

И пример теста:

public class DemoRepositoryTests {     private readonly Fixture _fixture;     private readonly Mock<IIgniteClient> _igniteClient;     private readonly DemoRepository _repository;     private readonly Mock<ICacheClient<int, DemoModel>> _cachClient;      public DemoRepositoryTests()     {         _fixture = new Fixture();         _igniteClient = new Mock<IIgniteClient>();         _repository = new DemoRepository(             _igniteClient.Object         );         _cachClient = new Mock<ICacheClient<int, DemoModel>>();     }      [Fact]     public void When_Get_then_success_test()     {         // Arrange         var demoModel1 = _fixture.Create<DemoModel>();         var demoModel2 = _fixture.Create<DemoModel>();         var demoModel3 = _fixture.Create<DemoModel>();         var cacheList = new List<ICacheEntry<int, DemoModel>>         {             new CacheEntry<int, DemoModel>(demoModel1.Id, demoModel1),             new CacheEntry<int, DemoModel>(demoModel2.Id, demoModel2),             new CacheEntry<int, DemoModel>(demoModel3.Id, demoModel3),         };          var filter = new DemoFilter(             ids: new[] { demoModel1.Id },             pageIndex: 0,             pageSize: 2         );          _igniteClient             .Setup(                 item => item.GetOrCreateCache<int, DemoModel>(                     It.IsAny<string>()                 )             )             .Returns(_cachClient.Object);          _cachClient             .Setup(                 item => item.AsCacheQueryable()             )             .Returns(cacheList.AsQueryable());          // Act         var result = _repository.Get(             filter         );          // Assert         // Проверим, что найден только один нужный нам элемент.         Assert.Single(result);         // Проверим, что нужный нам элемент находится в ответе.         Assert.Contains(result, model => model.Id == demoModel1.Id);         // Проверим, что элементы, которые не соответствуют условию, в ответе не содержаться.         Assert.DoesNotContain(result, model => model.Id == demoModel2.Id                                                || model.Id == demoModel3.Id);     } 

Проблема

На первый взгляд все просто: мокнули GetOrCreateCache и AsCacheQueryable и написали проверку правильной выборки. С моком метода GetOrCreateCache проблем нет, т.к. он есть в интерфейсе. А вот с AsCacheQueryable будет проблема, т.к. этот метод статический, и мы не можем его ни мокнуть, ни переопределить.

public static IQueryable<ICacheEntry<TKey, TValue>> AsCacheQueryable<TKey, TValue>(       this ICacheClient<TKey, TValue> cache) 

Решение

Для начала сделаем обвязку над ICacheClient, в котором используется статический метод AsCacheQueryable.

/// <summary> /// Обертка кэша Ignite. /// </summary> public interface IDemoCacheWrapper<TK, TV> : ICacheClient<TK, TV> { /// <summary> /// Получить доступ к кэшу через IQueryable. /// </summary> IQueryable<ICacheEntry<TK, TV>> AsQueryable(); } 

Затем сделаем обвязку над IgniteClient, чтобы метод GetOrCreateCache возвращал нам IDemoCacheWrapper.

/// <summary> /// Обертка над Ignite. /// </summary> public interface IDemoIgniteWrapper { /// <summary> /// Получить или создать кэш. /// </summary> IDemoCacheWrapper<TK, TV> GetOrCreateCache<TK, TV>( string name ); } 

И доработаем репозиторий

/// <summary> /// Доработанный репозиторий работающий с обертками над Ignite /// </summary> public class V2DemoRepository  { private readonly IDemoIgniteWrapper _igniteWrapper;      public V2DemoRepository(IDemoIgniteWrapper igniteWrapper)     {         _igniteWrapper = igniteWrapper;     }  public IList<DemoModel> Get( DemoFilter filter ) {         // Получаем существующий кэш с указанным именем или создаем новый, используя конфигурацию шаблона.         var cache = _igniteWrapper.GetOrCreateCache<int, DemoModel>(nameof(DemoModel));          var cacheData = cache             // Получаем данные из кэша.             // Результирующий запрос будет преобразован в запрос SQL кэша.              .AsQueryable()             .Where(                 item =>                     // Фильтруем данные по Id.                     filter.Ids == null || filter.Ids.Contains(item.Value.Id)             )             // Сортируем.             .OrderBy(item => item.Key)             // Используем пейджинг: Skip - сколько данных пропустить, Take - количество получаемых данных.             .Skip(filter.PageSize * filter.PageIndex)             .Take(filter.PageSize);          return cacheData             .Select(item => item.Value)             .ToList();     } }  /// <summary> /// Обертка над Ignite реализация. /// </summary> public class DemoIgniteWrapper : IDemoIgniteWrapper { private IIgniteClient _igniteClient;      public DemoIgniteWrapper(IIgniteClient igniteClient)     {         _igniteClient = igniteClient;     }  public IDemoCacheWrapper<TK, TV> GetOrCreateCache<TK, TV>( string name ) {         var cache = _igniteClient.GetOrCreateCache<TK, TV>(name);         return new DemoCacheWrapper<TK, TV>(cache);     } }  /// <summary> /// Обертка кэша Ignite реализация /// </summary> public class DemoCacheWrapper<TK, TV> : IDemoCacheWrapper<TK, TV> { private readonly ICacheClient<TK, TV> _cacheClient;  public DemoCacheWrapper( ICacheClient<TK, TV> cacheClient ) { _cacheClient = cacheClient ?? throw new ArgumentNullException(nameof(cacheClient)); }      /// <summary>     /// Получить доступ к кэшу через IQueryable.     /// </summary>     public IQueryable<ICacheEntry<TK, TV>> AsQueryable() { return _cacheClient.AsCacheQueryable(); } 

Теперь осталось доработать тест

    public V2DemoRepositoryTests()     {         _fixture = new Fixture();         _igniteWrapper = new Mock<IDemoIgniteWrapper>();         _repository = new V2DemoRepository(             _igniteWrapper.Object         );         _cachClient = new Mock<IDemoCacheWrapper<int, DemoModel>>();     }      [Fact]     public void When_Get_Then_success_test()     {         // Arrange         var demoModel1 = _fixture.Create<DemoModel>();         var demoModel2 = _fixture.Create<DemoModel>();         var demoModel3 = _fixture.Create<DemoModel>();         var cacheList = new List<ICacheEntry<int, DemoModel>>         {             new CacheEntry<int, DemoModel>(demoModel1.Id, demoModel1),             new CacheEntry<int, DemoModel>(demoModel2.Id, demoModel2),             new CacheEntry<int, DemoModel>(demoModel3.Id, demoModel3),         };          var filter = new DemoFilter(             ids: new[] { demoModel1.Id },             pageIndex: 0,             pageSize: 2         );          // Мокаем создание кэша.         _igniteWrapper             .Setup(                 item => item.GetOrCreateCache<int, DemoModel>(                     It.IsAny<string>()                 )             )             .Returns(_cachClient.Object);          // Мокаем получение данных кэша.         _cachClient             .Setup(                 item => item.AsQueryable()             )             .Returns(cacheList.AsQueryable());          // Act         var result = _repository.Get(             filter         );          // Assert         // Проверим, что найден только один нужный нам элемент.         Assert.Single(result);         // Проверим, что нужный нам элемент находится в ответе.         Assert.Contains(result, model => model.Id == demoModel1.Id);         // Проверим, что элементы, которые не соответствуют условию, в ответе не содержаться         Assert.DoesNotContain(result, model => model.Id == demoModel2.Id                                                || model.Id == demoModel3.Id);     } } 

Заключение

Таким образом, мы смогли протестировать логику, которая была зависима от статических методов. Если вы в своей работе сталкивались с подобными задачами, поделитесь в комментариях своими решениями. Буду рад узнать новое или ответить на ваши вопросы!


ссылка на оригинал статьи https://habr.com/ru/articles/894502/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *