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

Dan*_*rth 16 .net c# nhibernate orm unit-testing

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

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

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

用法如下:

var result = session.Query<Invoice>().ByInvoiceType(InvoiceType.NormalInvoice);
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 我不能嘲笑,ByInvoiceType因为它是一种扩展方法,或者我可以吗?
  • 我甚至无法嘲笑Query同样的理由.

Dan*_*rth 30

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

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

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

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

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);
Run Code Online (Sandbox Code Playgroud)

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

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

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

一些代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

    // ...
} 
Run Code Online (Sandbox Code Playgroud)

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

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