8 мая 2009 г.

TDD и LLBLGen Pro v 2.0

Сейчас мы активно используем ORM LLBLGen Pro в своих проектах. Я уже рассказывал, как мы решили проблему с тестированием DataAccessApater и запросов на Linq, которые по своему реализованы в этой ORM.

С тех пор прошло уже пол года и мы усовершенствовали этот подход. Проблема осталась та же: использование LLBLGen Pro в проекте и его уживаемость с модульным тестированием. В прошлый раз у нас получился такой результат:

   1:  using (var adapter = DataAccessAdapterFactory.Create())
   2:  {
   3:      ILinqMetaDataAdapter linqMetaData = LinqMetaDataFactory.Create(adapter);
   4:      result = linqMetaData.Query<realtyentity>().ToList();
   5:  }

Этот вариант оказался очень практичен и успешно используется на одном из проектов. Обе фабрики внутри себя содержали вызов ServiceLocator и возвращали разные реализации для тестов и нормального запуска.

Итак, мы начали новый проект и захотелось облегчить себе жизнь. Писать каждый раз создание двух объектов IDataAccessAdapter и ILinqMetadataAdapter через фабрики и потом в тесках делать для них заглушки довольно утомительно. Поэтому мы пришли к единому интерфейсу для доступа к данным:

   1:  public interface IDataContext : IDataAccessAdapter
   2:  {
   3:      IQueryable<T> Query<T>() where T : IEntityCore;
   4:  }

Как видно, он уже сочетает в себе и непосредственные функции адаптера, и доступ к IQueryable для запросов на Linq. Реальная реализация этого интерфейса выглядит довольно просто:

   1:  internal class LLBLGenDataContext : IDataContext
   2:  {
   3:      private readonly DataAccessAdapter adapter;
   4:      private readonly Dictionary<Type, Func<IQueryable>> entityLookup;
   5:      private readonly LinqMetaData linq;
   6:   
   7:      public LLBLGenDataContext()
   8:      {
   9:          adapter = new DataAccessAdapter();
  10:          linq = new LinqMetaData(adapter);
  11:          entityLookup = new Dictionary<Type, Func<IQueryable>>
  12:                             {
  13:                                 {typeof (AccountEntity), () => linq.Account}
  14:                             };
  15:      }
  16:   
  17:      #region IDataContext Members
  18:   
  19:      public IQueryable<T> Query<T>() where T : IEntityCore
  20:      {
  21:          IQueryable<T> result;
  22:          try
  23:          {
  24:              result = (IQueryable<T>) entityLookup[typeof (T)].Invoke();
  25:          }
  26:          catch (KeyNotFoundException ex)
  27:          {
  28:              throw new KeyNotFoundException(GlobalResources.LinqMetadataAdapterNotContainsNecessaryKeyExceptionMessage, ex);
  29:          }
  30:          return result;
  31:      }
  32:   
  33:      public void Dispose()
  34:      {
  35:          adapter.Dispose();
  36:      }
  37:   
  38:      public IPredicateExpression CreatePrimaryKeyFilter(IList primaryKeyFields)
  39:      {
  40:          return adapter.CreatePrimaryKeyFilter(primaryKeyFields);
  41:      }
  42:   
  43:      // Дальше идут функции обычного адаптера, которые передаются созданному внутри объекту adapter

Теперь в тестах мы пишем:

   1:  [Fact]
   2:  public void RemoveOldActivationsOnNewCreating()
   3:  {
   4:      mockDataContext.Setup(m => m.Query<AccountActivationEntity>())
   5:          .Returns(new List<AccountActivationEntity> {new AccountActivationEntity {AccountId = 1}, new AccountActivationEntity {AccountId = 1}}.AsQueryable());
   6:   
   7:      var repository = new AccountRepository();
   8:      repository.CreateActivation(new AccountEntity { Id = 1 });
   9:   
  10:      mockDataContext.Verify(d => d.DeleteEntity(It.IsAny<AccountActivationEntity>()), Times.Exactly(2));
  11:  }

Само хранилище AccountRepository использует интерфейс IDataContext для доступа к данным:

   1:  using(var context = Builder.Create<IDataContext>())
   2:  {
   3:      context.Query<AccountActivationEntity>().Where(...
   4:  }

Объект Builder работает через Ninject, опять же выдавая разные реализации для тестов и для реального использования.

Получилось на много удобнее, чем было раньше. Посмотрим во что это вырастет еще через пол года ;)

2 комментария:

  1. Я считаю, что не стоило делать IDataContext потомком IDataAccessAdapter, а вынести только необходимые методы, например CRUD :)

    ОтветитьУдалить
  2. @hazzik
    Да, кстати, можно только нужные методы оставить. Ну исходники у тебя под рукой, можешь делать как тебе больше нравится ;)

    ОтветитьУдалить