简体   繁体   English

使用扩展方法中定义的查询进行单元测试

[英]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 . 使用可以返回任何类型且不绑定到一种类型的通用存储库,即IRepository.Get<T>而不是IRepository<T>.Get NHibernates ISession is an example of such a repository. NHibernates ISession就是这样一个存储库的一个例子。
  2. Use extension methods on IQueryable<T> with a specific T to encapsulate recurring queries, eg 使用具有特定T IQueryable<T>上的扩展方法来封装重复查询,例如

     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 查询返回0个发票
  2. The query returns 1 invoice 查询返回1张发票
  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? 我不能模拟ByInvoiceType因为它是一个扩展方法,或者我可以吗?
  • I can't even mock Query for the same reason. 我出于同样的原因甚至无法模拟Query

After some more research and based on the answers here and on these links , I decided to completely re-design my API. 经过一些研究并根据这里的答案和这些 链接 ,我决定完全重新设计我的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. Mark的博客文章中列出的问题不再可能发生。 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. 业务层不再需要有关用于了解IQueryable<T>上允许哪些操作以及哪些操作不允许的数据存储的隐式知识。

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 . 正如Vytautas Mackonis在他的回答中所说,我不再直接依赖于NHibernate的ISession ,而是依赖于IRepository

This interface has a property named Query of type IQueries . 此接口具有名为Query of IQueries类型的IQueries For each entity the business layer needs to query there is a property in IQueries . 对于业务层需要查询的每个实体, 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. 每个查询接口都实现了通用的IQuery<T>接口,该接口又实现了IEnumerable<T> ,从而产生了如上所述的非常干净的DSL语法。

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. 这种流畅的查询语法允许业务层组合提供的查询,以充分利用底层ORM的功能,让数据库尽可能地过滤。

The implementation for NHibernate would look something like this: 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();
    }

    // ...
} 

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. 或者可以使用条件API实现其中一个查询,或者从另一个数据源获取数据。 The business layer would be oblivious to these details. 业务层将忽略这些细节。

ISession would be the thing you should mock in this case. 在这种情况下,你应该嘲笑ISession。 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. 它像在类中使用SqlConnection一样杀死可测试性 - 然后你必须“模拟”数据库本身。

Wrap ISession with some interface and it all becomes easy: 用一些界面包装ISession,一切都变得简单:

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. 然后你可以通过返回一个简单的列表来模拟IDataStore。

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. 使用像NHibernate这样的OR-Mapper模式的关键是让你的规范公开一个表达式树,ORM的Linq提供者可以解析它。 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() . 答案是(IMO):你应该模拟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. 需要注意的是:我完全忽略了Query在这里的定义 - 我甚至不知道NHibernate,以及它是否被定义为虚拟。

But it probably doesn't matter!Basically what I would do is: 但它可能没关系!基本上我要做的是:

-Mock Query to return a mock IQueryable. -Mock Query返回模拟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. (如果你不能模拟Query,因为它不是虚拟的,那么创建你自己的接口ISession,它暴露一个可模拟的查询,依此类推。) - 模拟IQueryable实际上并不分析它传递的查询,它只返回一些您在创建模拟时指定的预定结果

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: 有关执行扩展方法查询和简单模拟IQueryable实现的一般概念的更多信息,请参见此处:

http://blogs.msdn.com/b/tilovell/archive/2014/09/12/how-to-make-your-ef-queries-testable.aspx 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. 在List()中创建一个发票列表,其中包含3个测试中每个测试的预定义值,然后在fakeInvoiceList.AsQueryable()上调用扩展方法并测试结果。

Create entities in memory in a fakeList. 在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!). 我将您的IRepository视为“UnitOfWork”,将您的IQueries视为“存储库”(也许是一个流畅的存储库!)。 So, simply follow the UnitOfWork and Repository pattern. 因此,只需遵循UnitOfWork和Repository模式。 This is a good practice for EF but you can easily implement your own. 这对EF来说是一个很好的做法,但您可以轻松实现自己的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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