NSubstitute - тестирование определенного выражения linq
Я использую шаблон репозитория в приложении 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
?
Любое руководство будет оценено.
3 ответа
Очень короткий ответ - нет, 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));
}
Я поместил выражение в статический класс Queries, но вы можете использовать фабрику, чтобы лучше его инкапсулировать. Поскольку у вас есть ссылка на фактическое используемое выражение, вы можете установить возвращаемые значения и проверить, что вызовы были приняты как обычно. Вы также можете проверить выражение в изоляции.
Второй вариант делает это немного дальше, используя шаблон спецификации. Скажем, вы добавляете следующий элемент в интерфейс IRepository и вводите спецификацию IS:
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. Вы можете попробовать написать тесты, которые вы хотели бы иметь, а затем посмотреть, какой дизайн вы можете использовать для поддержки этой тестируемости.
Надеюсь это поможет.
Я наткнулся на этот вопрос, когда пытался выяснить, как вернуть конкретное значение, используя лямбда-выражение в NSubstitute. Тем не менее, для моего случая использования мне все равно, что на самом деле передается в запрос linq, и я хотел бы поделиться, как вернуть значения для запросов linq на поддельных интерфейсах в NSubstitute.
Итак, используя пример сверху
[Test]
public void TestUnprocessedInvoices()
{
IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
_invoiceRepository.Find(Arg.Any<Expression<Func<Invoice, bool>>>()).Returns(expectedResults);
}
Есть способ сделать это, сравнивая лямбда-выражения на равенство. Здесь был написан очень популярный ответ на связанный вопрос, который приводит пример класса 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()
Я не хотел отказываться от использования 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();
}
}
}