简体   繁体   中英

Unit testing with queries defined in extension methods

In my project I am using the following approach to querying data from the database:

  1. Use a generic repository that can return any type and is not bound to one type, ie IRepository.Get<T> instead of IRepository<T>.Get . NHibernates ISession is an example of such a repository.
  2. Use extension methods on IQueryable<T> with a specific T to encapsulate recurring queries, eg

     public static IQueryable<Invoice> ByInvoiceType(this IQueryable<Invoice> q, InvoiceType invoiceType) { return q.Where(x => x.InvoiceType == invoiceType); } 

Usage would be like this:

var result = session.Query<Invoice>().ByInvoiceType(InvoiceType.NormalInvoice);

Now assume I have a public method I want to test that uses this query. I want to test the three possible cases:

  1. The query returns 0 invoices
  2. The query returns 1 invoice
  3. The query returns multiple invoices

My problem now is: What to mock?

  • I can't mock ByInvoiceType because it is an extension method, or can I?
  • I can't even mock Query for the same reason.

After some more research and based on the answers here and on these links , I decided to completely re-design my API.

The basic concept is to completely disallow custom queries in the business code. This solves two problems:

  1. The testability is improved
  2. The problems outlined in Mark's blog post can no longer happen. The business layer no longer needs implicit knowledge about the datastore being used to know which operations are allowed on the IQueryable<T> and which are not.

In the business code, a query now looks like this:

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);

In practice this is implemented like this:

As Vytautas Mackonis suggested in his answer , I am no longer depending directly on NHibernate's ISession , instead I am now depending on an IRepository .

This interface has a property named Query of type IQueries . For each entity the business layer needs to query there is a property in IQueries . Each property has its own interface that defines the queries for the entity. Each query interface implements the generic IQuery<T> interface which in turn implementes IEnumerable<T> , leading to the very clean DSL like syntax seen above.

Some code:

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);
}

This fluent querying syntax allows the business layer to combine the supplied queries to take full advantage of the underlying ORM's capabilities to let the database filter as much as possible.

The implementation for NHibernate would look something like this:

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();
    }

    // ...
} 

In my real implementation I extracted most of the infrastructure code into a base class, so that it becomes very easy to create a new query object for a new entity. Adding a new query to an existing entity is also very simple.

The nice thing about this is that the business layer is completely free of querying logic and thus the data store can be switched easily. Or one could implement one of the queries using the criteria API or get the data from another data source. The business layer would be oblivious to these details.

ISession would be the thing you should mock in this case. But the real problem is that you should not have it as a direct dependency. It kills testability the same way as having SqlConnection in the class - you would then have to "mock" the database itself.

Wrap ISession with some interface and it all becomes easy:

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>();
    }
}

Then you could mock IDataStore by returning a simple list.

I know that this has long been answered and I do like the accepted answer, but for anybody running into a similar problem I would recommend looking into implementing the specification pattern as described here .

We've been doing this in our current project for more than a year now and everyone likes it. In most cases your repositories only need one method like

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

And that's very easy to mock.

Edit:

The key to using the pattern with an OR-Mapper like NHibernate is having your specifications expose an expression tree, which the ORM's Linq provider can parse. Please follow the link to the article I mentioned above for further details.

public interface ISpecification<T>
{
   Expression<Func<T, bool>> SpecExpression { get; }
   bool IsSatisfiedBy(T obj);
}

The answer is (IMO): you should mock Query() .

The caveat is: I say this in total ignorance of how Query is defined here - I don't even known NHibernate, and whether it is defined as virtual.

But it probably doesn't matter!Basically what I would do is:

-Mock Query to return a mock IQueryable. (If you can't mock Query because it's not virtual, then create your own interface ISession, which exposes a mockable query, and so on.) -The mock IQueryable doesn't actually analyze the query it is passed, it just returns some predetermined results that you specify when you create the mock.

All put together this basically lets you mock out your extension method whenever you want to.

For more about the general idea of doing extension method queries and a simple mock IQueryable implementation, see here:

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

To isolate testing just to just the extension method i wouldn't mock anything. Create a list of Invoices in a List() with predefined values for each of the 3 tests and then invoke the extension method on the fakeInvoiceList.AsQueryable() and test the results.

Create entities in memory in a fakeList.

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

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

// test results

If it suits your conditions, you can hijack generics to overload extension methods. Lets take the following example:

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
}

根据你的Repository.Get实现,你可以模拟NHibernate ISession。

I see your IRepository as a "UnitOfWork" and your IQueries as a "Repository" (Maybe a fluent repository!). So, simply follow the UnitOfWork and Repository pattern. This is a good practice for EF but you can easily implement your own.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM