使用扩展方法中定义的查询进行单元测试
本文关键字:查询 单元测试 定义 扩展 方法 | 更新日期: 2023-09-27 18:29:34
在我的项目中,我使用以下方法来查询数据库中的数据:
- 使用可以返回任何类型且不绑定到一种类型的通用存储库,即
IRepository.Get<T>
而不是IRepository<T>.Get
.NHibernatesISession
就是这种存储库的一个例子。 -
在具有特定
T
的IQueryable<T>
上使用扩展方法来封装重复查询,例如public static IQueryable<Invoice> ByInvoiceType(this IQueryable<Invoice> q, InvoiceType invoiceType) { return q.Where(x => x.InvoiceType == invoiceType); }
用法是这样的:
var result = session.Query<Invoice>().ByInvoiceType(InvoiceType.NormalInvoice);
现在假设我有一个要测试的公共方法,它使用此查询。我想测试三种可能的情况:
- 查询返回 0 张发票
- 查询返回 1 张发票
- 查询返回多张发票
我现在的问题是:嘲笑什么?
- 我不能嘲笑
ByInvoiceType
,因为它是一种扩展方法,或者我可以吗? - 出于同样的原因,我什至不能嘲笑
Query
。
经过更多的研究,并根据这里的答案和这些链接,我决定完全重新设计我的API。
基本概念是完全禁止业务代码中的自定义查询。这解决了两个问题:
- 提高了可测试性
- 马克博客文章中概述的问题不再会发生。业务层不再需要有关正在使用的数据存储的隐式知识来了解
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);
在实践中,这是这样实现的:
正如维陶塔斯·麦科尼斯(Vytautas Mackonis(在他的回答中所建议的那样,我不再直接依赖NHibernate的ISession
,而是现在依靠IRepository
。
此接口具有一个名为 Query
的属性,类型为 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);
}
这种流畅的查询语法允许业务层组合提供的查询,以充分利用底层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();
}
// ...
}
在我的实际实现中,我将大部分基础结构代码提取到基类中,以便为新实体创建新的查询对象变得非常容易。向现有实体添加新查询也非常简单。
这样做的好处是业务层完全没有查询逻辑,因此可以轻松切换数据存储。或者,可以使用条件 API 实现其中一个查询,或者从另一个数据源获取数据。业务层将忽略这些细节。
ISession将是你应该嘲笑的东西。但真正的问题是,你不应该把它作为一个直接的依赖关系。它杀死可测试性的方式与在类中使用 SqlConnection 的方式相同 - 然后您必须"模拟"数据库本身。
用一些界面包装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>();
}
}
然后,您可以通过返回一个简单的列表来模拟IDataStore。
为了将测试隔离到扩展方法,我不会嘲笑任何东西。 在 List(( 中创建一个发票列表,其中包含 3 个测试中每个测试的预定义值,然后在 fakeInvoiceList.AsQueryable(( 上调用扩展方法并测试结果。
在假列表的内存中创建实体。
var testList = new List<Invoice>();
testList.Add(new Invoice {...});
var result = testList().AsQueryable().ByInvoiceType(enumValue).ToList();
// test results
根据您对 Repository.Get 的实现,您可以模拟 NHibernate ISession。
如果它适合您的条件,您可以劫持泛型以重载扩展方法。让我们以以下示例为例:
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
}
我认为您的独立性是"工作单元",而您的IQueries是"存储库"(也许是一个流畅的存储库!因此,只需遵循工作单元和存储库模式即可。这是 EF 的好做法,但你可以轻松实现自己的做法。
这早已得到解答,我确实喜欢公认的答案,但对于遇到类似问题的任何人来说,我建议研究实现此处描述的规范模式。
我们已经在当前的项目中这样做了一年多,每个人都喜欢它。在大多数情况下,您的存储库只需要一种方法,例如
IEnumerable<MyEntity> GetBySpecification(ISpecification<MyEntity> spec)
这很容易被嘲笑。
编辑:
将模式与 OR-Mapper (如 NHibernate(一起使用的关键是让您的规范公开一个表达式树,ORM 的 Linq 提供程序可以解析该表达式树。请点击我上面提到的文章的链接以获取更多详细信息。
public interface ISpecification<T>
{
Expression<Func<T, bool>> SpecExpression { get; }
bool IsSatisfiedBy(T obj);
}
答案是(IMO(:你应该嘲笑Query()
。
需要注意的是:我这样说完全不知道查询在这里是如何定义的 - 我什至不知道 NHibernate,以及它是否被定义为虚拟的。
但这可能无关紧要!基本上我要做的是:
-模拟查询以返回模拟IQueryable。(如果因为 Query 不是虚拟的而无法模拟它,请创建自己的接口 ISession,该接口公开可模拟的查询,依此类推。-模拟 IQueryable 实际上并不分析它传递的查询,它只是返回您在创建模拟时指定的一些预定结果。
总而言之,这基本上可以让您随时模拟扩展方法。
有关执行扩展方法查询的一般思想和简单的模拟 IQueryable 实现的更多信息,请参阅此处:
http://blogs.msdn.com/b/tilovell/archive/2014/09/12/how-to-make-your-ef-queries-testable.aspx