使用扩展方法定义的查询进行unit testing

在我的项目中,我使用以下方法从数据库中查询数据:

  1. 使用可返回任何类型且不绑定到一种类型的通用存储库,即IRepository.Get而不是IRepository.Get 。 NHibernates ISession就是这样一个存储库的一个例子。
  2. 使用具有特定T IQueryable上的扩展方法来封装重复查询,例如

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

用法如下:

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

现在假设我有一个我要测试的公共方法使用此查询。 我想测试三种可能的情况:

  1. 查询返回0个发票
  2. 查询返回1张发票
  3. 查询返回多个发票

我现在的问题是:要嘲笑什么?

  • 我不能模拟ByInvoiceType因为它是一个扩展方法,或者我可以吗?
  • 我出于同样的原因甚至无法模拟Query

经过一些研究并根据这里的答案和这些 链接 ,我决定完全重新设计我的API。

基本概念是完全禁止业务代码中的自定义查询。 这解决了两个问题:

  1. 可测试性得到改善
  2. Mark的博客文章中列出的问题不再可能发生。 业务层不再需要有关用于了解IQueryable上允许哪些操作以及哪些操作不允许的数据存储的隐式知识。

在业务代码中,查询现在看起来像这样:

 IEnumerable inv = repository.Query .Invoices.ThatAre .Started() .Unfinished() .And.WithoutError(); // or IEnumerable inv = repository.Query.Invoices.ThatAre.Started(); // or Invoice inv = repository.Query.Invoices.ByInvoiceNumber(invoiceNumber); 

在实践中,这是这样实现的:

正如Vytautas Mackonis在他的回答中所说,我不再直接依赖于NHibernate的ISession ,而是依赖于IRepository

此接口具有名为Query of IQueries类型的IQueries 。 对于业务层需要查询的每个实体, IQueries都有一个属性。 每个属性都有自己的接口,用于定义实体的查询。 每个查询接口都实现了通用的IQuery接口,该接口又实现了IEnumerable ,从而产生了如上所述的非常干净的DSL语法。

一些代码:

 public interface IRepository { IQueries Queries { get; } } public interface IQueries { IInvoiceQuery Invoices { get; } IUserQuery Users { get; } } public interface IQuery : IEnumerable { T Single(); T SingleOrDefault(); T First(); T FirstOrDefault(); } public interface IInvoiceQuery : IQuery { IInvoiceQuery Started(); IInvoiceQuery Unfinished(); IInvoiceQuery WithoutError(); Invoice ByInvoiceNumber(string invoiceNumber); } 

这种流畅的查询语法允许业务层组合提供的查询,以充分利用底层ORM的function,让数据库尽可能地过滤。

NHibernate的实现看起来像这样:

 public class NHibernateInvoiceQuery : IInvoiceQuery { IQueryable _query; public NHibernateInvoiceQuery(ISession session) { _query = session.Query(); } 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 GetEnumerator() { return _query.GetEnumerator(); } // ... } 

在我的实际实现中,我将大部分基础结构代码提取到基类中,因此为新实体创建新的查询对象变得非常容易。 向现有实体添加新查询也非常简单。

关于这一点的好处是业务层完全没有查询逻辑,因此可以轻松切换数据存储。 或者可以使用条件API实现其中一个查询,或者从另一个数据源获取数据。 业务层将忽略这些细节。

在这种情况下,你应该嘲笑ISession。 但真正的问题是你不应该将它作为直接依赖。 它像在类中使用SqlConnection一样杀死可测试性 – 然后你必须“模拟”数据库本身。

用一些界面包装ISession,一切都变得简单:

 public interface IDataStore { IQueryable Query(); } public class NHibernateDataStore : IDataStore { private readonly ISession _session; public NHibernateDataStore(ISession session) { _session = session; } public IQueryable Query() { return _session.Query(); } } 

然后你可以通过返回一个简单的列表来模拟IDataStore。

为了将测试仅仅分离到扩展方法,我不会模仿任何东西。 在List()中创建一个发票列表,其中包含3个测试中每个测试的预定义值,然后在fakeInvoiceList.AsQueryable()上调用扩展方法并测试结果。

在fakeList中创建内存中的实体。

 var testList = new List(); testList.Add(new Invoice {...}); var result = testList().AsQueryable().ByInvoiceType(enumValue).ToList(); // test results 

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

如果它符合您的条件,您可以劫持generics来重载扩展方法。 让我们看看以下示例:

 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 test) where TTest : FakeSession { test.Query(); // calls the fake method } 

我将您的IRepository视为“UnitOfWork”,将您的IQueries视为“存储库”(也许是一个流畅的存储库!)。 因此,只需遵循UnitOfWork和Repository模式即可。 这是EF的一个好习惯,但您可以轻松实现自己的。

我知道这已经得到了很长时间的回答,我确实喜欢接受的答案,但是对于任何遇到类似问题的人,我建议我们考虑实现这里描述的规范模式

我们已经在我们当前的项目中做了一年多了,现在每个人都喜欢它。 在大多数情况下,您的存储库只需要一个方法

 IEnumerable GetBySpecification(ISpecification spec) 

这很容易嘲笑。

编辑:

使用像NHibernate这样的OR-Mapper模式的关键是让你的规范公开一个表达式树,ORM的Linq提供者可以解析它。 请点击上面提到的文章的链接以获取更多详细信息。

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

答案是(IMO):你应该模拟Query()

需要注意的是:我完全忽略了Query在这里的定义 – 我甚至不知道NHibernate,以及它是否被定义为虚拟。

但它可能没关系!基本上我会做的是:

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

所有这些基本上让你可以随时模拟你的扩展方法。

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

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