Групповое тестирование с запросами, определенными в методах расширения
В моем проекте я использую следующий подход для запроса данных из базы данных:
- Используйте общий репозиторий, который может возвращать любой тип и не привязан к одному типу, т.е.
IRepository.Get<T>
вместо IRepository<T>.Get
. NHibernates ISession
является примером такого репозитория.
-
Используйте методы расширения на IQueryable<T>
с определенным T
для инкапсуляции повторяющихся запросов, например
public static IQueryable<Invoice> ByInvoiceType(this IQueryable<Invoice> q,
InvoiceType invoiceType)
{
return q.Where(x => x.InvoiceType == invoiceType);
}
Использование будет таким:
var result = session.Query<Invoice>().ByInvoiceType(InvoiceType.NormalInvoice);
Теперь предположим, что у меня есть открытый метод, который я хочу проверить, который использует этот запрос. Я хочу проверить три возможных случая:
- Запрос возвращает 0 фактур
- Запрос возвращает 1 счет-фактуру
- Запрос возвращает несколько счетов-фактур
Теперь моя проблема: что насмехаться?
- Я не могу издеваться над
ByInvoiceType
, потому что это метод расширения, или я могу?
- Я не могу даже высмеять
Query
по той же причине.
Ответы
Ответ 1
После нескольких исследований и основанных на ответах здесь и на эти , я решил полностью перепроектировать свой API.
Основная концепция - полностью запретить пользовательские запросы в бизнес-коде. Это решает две проблемы:
- Улучшена тестируемость
- Проблемы, описанные в Отметить сообщение в блоге, больше не может быть. Бизнес-уровень больше не нуждается в неявных знаниях о хранилище данных, которое используется для определения того, какие операции разрешены на
IQueryable<T>
, а какие нет.
В бизнес-коде запрос теперь выглядит следующим образом:
IEnumerable<Invoice> inv = repository.Query
.Invoices.ThatAre
.Started()
.Unfinished()
.And.WithoutError();
// or
IEnumerable<Invoice> inv = repository.Query.Invoices.ThatAre.Started();
// or
Invoice inv = repository.Query.Invoices.ByInvoiceNumber(invoiceNumber);
На практике это выполняется следующим образом:
Как сказал Vytautas Mackonis в его ответе, я больше не зависим от NHibernate ISession
, вместо этого теперь я зависим от IRepository
.
Этот интерфейс имеет свойство с именем Query
типа IQueries
. Для каждого объекта, к которому необходимо запросить бизнес-уровень, есть свойство в IQueries
. Каждое свойство имеет свой собственный интерфейс, который определяет запросы для объекта. Каждый интерфейс запросов реализует общий IQuery<T>
интерфейс, который, в свою очередь, реализует IEnumerable<T>
, что приводит к очень чистому синтаксису DSL, показанному выше.
Некоторые коды:
public interface IRepository
{
IQueries Queries { get; }
}
public interface IQueries
{
IInvoiceQuery Invoices { get; }
IUserQuery Users { get; }
}
public interface IQuery<T> : IEnumerable<T>
{
T Single();
T SingleOrDefault();
T First();
T FirstOrDefault();
}
public interface IInvoiceQuery : IQuery<Invoice>
{
IInvoiceQuery Started();
IInvoiceQuery Unfinished();
IInvoiceQuery WithoutError();
Invoice ByInvoiceNumber(string invoiceNumber);
}
Этот свободный синтаксис запросов позволяет бизнес-уровню объединить предоставленные запросы, чтобы в полной мере использовать возможности ORM, позволяющие фильтровать базу данных как можно больше.
Реализация для NHibernate будет выглядеть примерно так:
public class NHibernateInvoiceQuery : IInvoiceQuery
{
IQueryable<Invoice> _query;
public NHibernateInvoiceQuery(ISession session)
{
_query = session.Query<Invoice>();
}
public IInvoiceQuery Started()
{
_query = _query.Where(x => x.IsStarted);
return this;
}
public IInvoiceQuery WithoutError()
{
_query = _query.Where(x => !x.HasError);
return this;
}
public Invoice ByInvoiceNumber(string invoiceNumber)
{
return _query.SingleOrDefault(x => x.InvoiceNumber == invoiceNumber);
}
public IEnumerator<Invoice> GetEnumerator()
{
return _query.GetEnumerator();
}
// ...
}
В моей реальной реализации я извлек большую часть кода инфраструктуры в базовый класс, так что очень легко создать новый объект запроса для нового объекта. Добавление нового запроса к существующей сущности также очень просто.
Самое приятное в том, что бизнес-уровень полностью свободен от логики запросов, и, таким образом, хранилище данных можно легко переключить. Или можно реализовать один из запросов с использованием API критериев или получить данные из другого источника данных. Бизнес-уровень не будет забывать об этих деталях.
Ответ 2
В этом случае вы должны издеваться над ISession. Но реальная проблема заключается в том, что вы не должны иметь ее как прямую зависимость. Он убивает тестируемость так же, как и SqlConnection в классе, - тогда вам придется "издеваться" над самой базой данных.
Оберните ISession с помощью какого-либо интерфейса, и все станет легко:
public interface IDataStore
{
IQueryable<T> Query<T>();
}
public class NHibernateDataStore : IDataStore
{
private readonly ISession _session;
public NHibernateDataStore(ISession session)
{
_session = session;
}
public IQueryable<T> Query<T>()
{
return _session.Query<T>();
}
}
Затем вы можете издеваться над IDataStore, возвращая простой список.
Ответ 3
Чтобы изолировать тестирование только до метода расширения, я бы ничего не издевался. Создайте список счетов-фактур в списке() с предопределенными значениями для каждого из трех тестов, а затем вызовите метод расширения на fakeInvoiceList.AsQueryable() и проверьте результаты.
Создание сущностей в памяти в fakeList.
var testList = new List<Invoice>();
testList.Add(new Invoice {...});
var result = testList().AsQueryable().ByInvoiceType(enumValue).ToList();
// test results
Ответ 4
Если это соответствует вашим условиям, вы можете захватить дженерики, чтобы перегрузить методы расширения. Возьмем следующий пример:
interface ISession
{
// session members
}
class FakeSession : ISession
{
public void Query()
{
Console.WriteLine("fake implementation");
}
}
static class ISessionExtensions
{
public static void Query(this ISession test)
{
Console.WriteLine("real implementation");
}
}
static void Stub1(ISession test)
{
test.Query(); // calls the real method
}
static void Stub2<TTest>(TTest test) where TTest : FakeSession
{
test.Query(); // calls the fake method
}
Ответ 5
в зависимости от вашей реализации Repository.Get, вы можете высмеять NHibernate ISession.
Ответ 6
Я вижу ваш IRepository как "UnitOfWork" и ваш IQueries в качестве "репозитория" (возможно, это бесплатный репозиторий!). Поэтому просто следуйте шаблону UnitOfWork и Repository. Это хорошая практика для EF, но вы можете легко реализовать свои собственные.
Ответ 7
Я знаю, что на это уже давно дан ответ, и мне нравится принятый ответ, но для любого, кто сталкивается с подобной проблемой, я бы рекомендовал изучить реализацию шаблон спецификации, как описано здесь.
Мы делаем это в нашем текущем проекте уже более года, и всем это нравится. В большинстве случаев вашим репозиториям нужен только один метод, например
IEnumerable<MyEntity> GetBySpecification(ISpecification<MyEntity> spec)
И это очень легко насмехаться.
Изменить:
Ключ к использованию шаблона с OR-Mapper, например, NHibernate, показывает, что ваши спецификации отображают дерево выражений, которое поставщик ORM Linq может анализировать. Пожалуйста, перейдите по ссылке на статью, упомянутую выше, для получения более подробной информации.
public interface ISpecification<T>
{
Expression<Func<T, bool>> SpecExpression { get; }
bool IsSatisfiedBy(T obj);
}
Ответ 8
Ответ: (IMO): вы должны высмеивать Query()
.
Предостережение: я говорю об этом в полном незнании того, как здесь задается Query - я даже не знаю NHibernate и определяется ли он как виртуальный.
Но это, вероятно, не имеет значения! В основном я бы сделал:
-Mock Query, чтобы вернуть mock IQueryable. (Если вы не можете издеваться над запросом, потому что он не виртуальный, то создайте свой собственный интерфейс ISession, который выдает макет запроса и т.д.)
- Макет IQueryable фактически не анализирует передаваемый запрос, он просто возвращает некоторые заранее определенные результаты, которые вы указываете при создании макета.
Все вместе, это в основном позволяет вам издеваться над вашим методом расширения, когда захотите.
Подробнее об общей идее выполнения запросов метода расширения и простой реализации в стиле IQueryable см. здесь:
http://blogs.msdn.com/b/tilovell/archive/2014/09/12/how-to-make-your-ef-queries-testable.aspx