NSubstitute - тестирование определенного выражения linq - PullRequest
9 голосов
/ 13 апреля 2011

Я использую шаблон репозитория в приложении MVC 3, которое я сейчас разрабатываю.Мой интерфейс репозитория выглядит следующим образом:

public interface IRepository<TEntity> where TEntity : IdEntity
{
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Remove(TEntity entity);
    TEntity GetById(int id);
    IList<TEntity> GetAll();
    TEntity FindFirst(Expression<Func<TEntity, bool>> criteria);
    IList<TEntity> Find(Expression<Func<TEntity, bool>> criteria);
}

Во многих случаях при кодировании методов в моих классах обслуживания я использую методы FindFirst и Find.Как видите, они оба принимают выражение linq в качестве входных данных.Я хочу знать, есть ли способ, с помощью которого NSubstitute позволяет вам указать конкретное выражение, которое вы хотите проверить в своем коде.

Итак, вот пример сервисного метода, который иллюстрирует использование одного из методов репозитория, которые я упомянул:

public IList<InvoiceDTO> GetUnprocessedInvoices()
{
    try
    {
        var invoices = _invoiceRepository.Find(i => !i.IsProcessed && i.IsConfirmed);
        var dtoInvoices = Mapper.Map<IList<Invoice>, IList<InvoiceDTO>>(invoices);
        return dtoInvoices;
    }
    catch (Exception ex)
    {
        throw new Exception(string.Format("Failed to get unprocessed invoices: {0}", ex.Message), ex);
    }
}

Итак, есть ли способ, используя NSubtitute, чтоЯ могу проверить конкретное выражение лямда: i => !i.IsProcessed && i.IsConfirmed?

Будем благодарны за любые указания.

Ответы [ 4 ]

13 голосов
/ 14 апреля 2011

Очень короткий ответ - нет, NSubstitute не имеет ничего построенного, чтобы упростить тестирование определенных выражений.

Гораздо более длинный ответ заключается в том, что есть несколько вариантов, которые вы можете попробовать, и большинство из них подразумевают избегание прямого использования LINQ в тестируемом классе. Я не уверен, являются ли какие-либо из этих идей хорошими, поскольку я не знаю полного контекста, но, надеюсь, здесь будет некоторая информация, которую вы сможете использовать. В следующих примерах я исключил шаг Mapper, чтобы сделать образцы кода немного меньше.

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

//Class under test uses:
_invoiceRepository.Find(Queries.UnprocessedConfirmedOrders)

[Test]
public void TestUnprocessedInvoices()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Queries.UnprocessedConfirmedOrders).Returns(expectedResults);
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
}

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

Второй вариант делает это немного дальше, используя шаблон спецификации. Допустим, вы добавили в интерфейс IRepository следующий элемент и добавили ISpecification:

public interface IRepository<TEntity> where TEntity : IdEntity
{
   /* ...snip... */
    IList<TEntity> Find(ISpecification<TEntity> query);
}

public interface ISpecification<T> { bool Matches(T item);  }

Затем вы можете проверить это так:

//Class under test now uses:
_invoiceRepository.Find(new UnprocessedConfirmedOrdersQuery());

[Test]
public void TestUnprocessedInvoicesUsingSpecification()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Arg.Any<UnprocessedConfirmedOrdersQuery>()).Returns(expectedResults);
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
}

Опять же, вы можете протестировать этот запрос изолированно, чтобы убедиться, что он выполняет то, что вы думаете.

Третий вариант - перехватить используемый аргумент и проверить его напрямую. Это немного грязно, но работает:

[Test]
public void TestUnprocessedInvoicesByCatchingExpression()
{
    Expression<Func<InvoiceDTO, bool>> queryUsed = null;
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository
        .Find(i => true)
        .ReturnsForAnyArgs(x =>
        {
            queryUsed = (Expression<Func<InvoiceDTO, bool>>)x[0];
            return expectedResults;
        });

    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
    AssertQueryPassesFor(queryUsed, new InvoiceDTO { IsProcessed = false, IsConfirmed = true });
    AssertQueryFailsFor(queryUsed, new InvoiceDTO { IsProcessed = true, IsConfirmed = true });
}

(надеюсь, в будущих версиях NSubstitute это будет немного легче)

Четвертый вариант - найти / одолжить / написать / украсть некоторый код, который может сравнивать деревья выражений, и использовать Arg.Is (...) NSubstitute, который принимает предикат для сравнения деревьев выражений.

Пятый вариант - это не модульное тестирование до такой степени, а просто интеграционное тестирование с использованием реального InvoiceRepository. Вместо того, чтобы беспокоиться о механике происходящего, попробуйте проверить фактическое поведение, которое вам требуется.

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

Надеюсь, это поможет.

5 голосов
/ 13 июня 2011

Я наткнулся на этот вопрос, когда пытался выяснить, как вернуть конкретное значение, используя лямбда-выражение в NSubstitute.Однако для моего случая использования мне все равно, что на самом деле передается в запрос linq, и я хотел бы поделиться с вами тем, как возвращать значения для запросов linq по имитированным интерфейсам в NSubstitute.

Так что, используя пример выше1003 *

[Test]
public void TestUnprocessedInvoices()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Arg.Any<Expression<Func<Invoice, bool>>>()).Returns(expectedResults);
}
3 голосов
/ 30 сентября 2014

Я не хотел отказываться от использования Expression<Func<T,bool>> в моем интерфейсе репозитория, поэтому в качестве альтернативы программированию этого конкретного макета (поскольку NSubstitute не поддерживал его), я просто создал закрытый класс в своем тестеСветильник, который реализовал мой интерфейс репозитория и только метод, связанный с Expression, который будет использовать тест.Я смог продолжить использовать NSubstitute для проверки всех других зависимостей, как обычно, но я мог использовать этот же репозиторий для нескольких разных тестов и фактически получать разные результаты из разных входных данных.

public class SomeFixture
{
    private readonly IRepository<SomeEntity> entityRepository;
    private readonly IRepository<SomeThing> thingRepository;

    public SomeFixture()
    {
        var entities = new List<SomeEntity>
        {
            BuildEntityForThing(1),
            BuildEntityForThing(1),
            BuildEntityForThing(1),
            BuildEntityForThing(2),
        };
        entityRepository = new FakeRepository(entities);

        thingRepository = Substitute.For<IRepository<SomeThing>>();
        thingRepository.GetById(1).Returns(BuildThing(1));
        thingRepository.GetById(2).Returns(BuildThing(2));
    }

    public void SomeTest()
    {
        var classUnderTest = new SomeClass(thingRepository, entityRepository);

        Assert.AreEqual(classUnderTest.FetchEntitiesForThing(1).Count, 3);
    }

    private void SomeOtherTest()
    {
        var classUnderTest = new SomeClass(thingRepository, entityRepository);

        Assert.AreEqual(classUnderTest.FetchEntitiesForThing(2).Count, 1);
    }

    private class FakeRepository : IRepository<SomeEntity>
    {
        private readonly List<SomeEntity> items;

        public FakeRepository(List<SomeEntity> items)
        {
            this.items = items;
        }

        IList<TEntity> Find(Expression<Func<SomeEntity, bool>> criteria)
        {
            // For these purposes, ignore possible inconsistencies 
            // between Linq and SQL when executing expressions
            return items.Where(criteria.Compile()).ToList();
        }

        // Other unimplemented methods from IRepository ...
        void Add(SomeEntity entity)
        {
            throw new NotImplementedException();
        }
    }
}
0 голосов
/ 28 марта 2019

Есть способ сделать это, сравнивая лямбда-выражения на равенство. Очень популярный ответ был написан для связанного вопроса здесь , который дает пример класса LambdaCompare.

Затем вы можете использовать это LambdaCompare для проверки равенства или лямбды на равенство в вашей фиктивной установке:

var mockRepository = Substitute.For<IRepository>();
mockRepository.Find(Arg.Is<Expression<Func<Invoice, bool>>>(expr =>
                    LambdaCompare.Eq(expr, i => !i.IsProcessed && i.IsConfirmed))
              .Returns(..etc..)

Только если фиктивный репозиторий .Find() вызывается с выражением i => !i.IsProcessed && i.IsConfirmed, он вернет то, что было указано в .Returns()

...