NSubstitute - Testing for a specific linq expression
I stumbled upon this question when I was trying to figure out how to return a specific value using a lambda expression in NSubstitute. However, for my use case I don't care what is actually passed into the linq query, and wanted to share how to return values for linq queries on mocked interfaces in NSubstitute.
So using the example from above
[Test]
public void TestUnprocessedInvoices()
{
IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
_invoiceRepository.Find(Arg.Any<Expression<Func<Invoice, bool>>>()).Returns(expectedResults);
}
The very short answer is no, NSubstitute doesn't have anything built it to make testing specific expressions easier.
The much longer answer is that there are a few options you can try, and most of them involve avoiding direct use of LINQ in the class under test. I'm not sure if any of these are good ideas as I don't know the full context, but hopefully there will be some info here you can use. In the following examples I've eliminated the Mapper step to make the code samples a bit smaller.
First option is to make it so you can check the expression is the same reference you are expecting, which means you can no longer create it directly in your code under test. For example:
//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));
}
I've dumped the expression on a static Queries class, but you could use a factory to encapsulate it better. Because you have an reference to the actual expression used you can set return values and check calls were received as normal. You can also test the expression in isolation.
Second option takes this a bit further by using a specification pattern. Say you add the following member to the IRepository interface and introduce an ISpecification:
public interface IRepository<TEntity> where TEntity : IdEntity
{
/* ...snip... */
IList<TEntity> Find(ISpecification<TEntity> query);
}
public interface ISpecification<T> { bool Matches(T item); }
You can then test it like this:
//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));
}
Again, you can test this query in isolation to make sure it does what you think.
Third option is to catch the argument used and test it directly. This is a bit messy but works:
[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 });
}
(This will hopefully be getting a bit easier in future NSubstitute versions)
Fourth option would be to find/borrow/write/steal some code that can compare expression trees, and use NSubstitute's Arg.Is(...) that takes a predicate to compare the expression trees there.
Fifth option is to not unit test it to that degree, and just integration test using a real InvoiceRepository. Rather than worry about the mechanics of what's happening, try verifying the actual behaviour you require.
My general advice would be to look at exactly what you need to test and how you can best and most easily write those tests. Remember that both the expression and the fact that it is passed through needs to be tested somehow, and the test need not be a unit test. It may also be worth considering whether the current IRepository interface is making your life easier. You could try writing the tests you would like to have, then see what design you can drive out to support that testability.
Hope this helps.