Модульное тестирование с запросами, определенными в методах расширения - PullRequest
16 голосов
/ 29 марта 2012

В моем проекте я использую следующий подход для запроса данных из базы данных:

  1. Используйте универсальный репозиторий, который может возвращать любой тип и не привязан к одному типу, т.е. IRepository.Get<T>IRepository<T>.Get.NHibernates ISession является примером такого хранилища.
  2. Используйте методы расширения для 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);

Теперь предположим, что у меня есть публичный метод, который я хочу протестировать, который использует этот запрос.Я хочу протестировать три возможных случая:

  1. Запрос возвращает 0 счетов
  2. Запрос возвращает 1 счет
  3. Запрос возвращает несколько счетов

Моя проблема сейчас: что подшучивать?

  • Я не могу насмехаться ByInvoiceType, потому что это метод расширения, или я могу?
  • Я даже не могу высмеивать Query по той же причине.

Ответы [ 8 ]

30 голосов
/ 30 марта 2012

После некоторых исследований и на основе ответов здесь и на этих ссылках я решил полностью изменить дизайн моего API.

Основная концепция заключается в том, чтополностью запретить пользовательские запросы в бизнес-коде.Это решает две проблемы:

  1. Улучшена тестируемость
  2. Проблемы, описанные в сообщении Марка , больше не могут возникнуть.Бизнес-уровень больше не нуждается в неявных знаниях о том, какое хранилище данных используется, чтобы узнать, какие операции разрешены в 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);

На практике это реализовано так:

Как Витаутас Макконис предложил в свой ответ , я больше не зависел напрямую от ISession NHibernate, вместо этогоЯ сейчас в зависимости от 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 голосов
/ 30 марта 2012

ISession - это то, что вы должны высмеивать в этом случае.Но настоящая проблема в том, что вы не должны иметь это как прямую зависимость.Это убивает тестируемость так же, как и наличие SqlConnection в классе - тогда вам придется «насмехаться» над самой базой данных.

Обернуть ISession некоторым интерфейсом, и все становится легко:* Тогда вы можете издеваться над IDataStore, возвращая простой список.

0 голосов
/ 15 сентября 2014

Ответ (ИМО): вы должны издеваться Query().

Предостережение: я говорю это в полном незнании того, как здесь определен Query - я даже не знаю NHibernate, и определяется ли он как виртуальный.

Но это, вероятно, не имеет значения! По сути, я бы сделал следующее:

-Mock Query для возврата макета IQueryable. (Если вы не можете макетировать Query, потому что он не виртуальный, создайте собственный интерфейс ISession, который выставляет фиктивный запрос и т. Д.) -Модельный IQueryable фактически не анализирует запрос, который он передал, он просто возвращает некоторые предопределенные результаты , которые вы указываете при создании макета.

Все вместе это в принципе позволяет вам смоделировать ваш метод расширения в любое время.

Подробнее об общей идее выполнения запросов метода расширения и простой фиктивной реализации IQueryable см. Здесь:

http://blogs.msdn.com/b/tilovell/archive/2014/09/12/how-to-make-your-ef-queries-testable.aspx

0 голосов
/ 24 июля 2013

Я знаю, что на этот вопрос уже давно дан ответ, и мне нравится принятый ответ, но всем, кто сталкивается с подобной проблемой, я бы рекомендовал изучить реализацию шаблона спецификации , как описано здесь .

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

IEnumerable<MyEntity> GetBySpecification(ISpecification<MyEntity> spec)

И это очень легко смоделировать.

Редактировать:

Ключ к использованию шаблона с OR-Mapper, как NHibernate, заставляет ваши спецификации предоставлять дерево выражений, которое может проанализировать поставщик Linq из ORM.Пожалуйста, перейдите по ссылке на статью, которую я упомянул выше для получения дополнительной информации.

public interface ISpecification<T>
{
   Expression<Func<T, bool>> SpecExpression { get; }
   bool IsSatisfiedBy(T obj);
}
0 голосов
/ 31 марта 2012

Я вижу ваш IRepository как «UnitOfWork», а ваши IQueries - как «Repository» (возможно, свободное хранилище!). Итак, просто следуйте шаблону UnitOfWork и Repository. Это хорошая практика для EF , но вы можете легко реализовать свою собственную.

0 голосов
/ 29 марта 2012

в зависимости от вашей реализации Repository.Get, вы можете издеваться над NHibernate ISession.

0 голосов
/ 29 марта 2012

Если это соответствует вашим условиям, вы можете перехватить дженерики для перегрузки методов расширения.Давайте рассмотрим следующий пример:

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
}
0 голосов
/ 29 марта 2012

Чтобы изолировать тестирование только от метода расширения, я бы ничего не издевался. Создайте список счетов-фактур в List () с предопределенными значениями для каждого из 3 тестов, а затем вызовите метод расширения для fakeInvoiceList.AsQueryable () и протестируйте результаты.

Создание сущностей в памяти в fakeList.

var testList = new List<Invoice>();
testList.Add(new Invoice {...});

var result = testList().AsQueryable().ByInvoiceType(enumValue).ToList();

// test results
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...