人们如何使用entity framework6进行unit testing,你应该打扰吗?

总的来说,我刚开始使用unit testing和TDD。 我已经涉足了,但现在我决定把它添加到我的工作stream程,并写出更好的软件。

我昨天提出了一个问题,那就是包括了这个问题,但这似乎是一个问题。 我已经坐下来开始实施一个服务类,我将用它来从控制器中抽象出业务逻辑,并映射到使用EF6的特定模型和数据交互。

问题是我已经阻止了自己,因为我不想在存储库中抽象EF(它仍然可以在特定查询的服务之外使用),并且想testing我的服务(将使用EF上下文) 。

在这里,我想是问题,有没有这样的一个点? 如果是这样的话,那么由于IQueryable引起的漏洞抽象,以及Ladislav Mrnka在unit testing这个主题上的许多重要的post都不是直接的,因为Linq提供者在处理内存实现为特定的数据库。

我想testing的代码看起来很简单。 (这只是虚拟代码,试图了解我在做什么,我想用TDD驱动创build)

上下文

public interface IContext { IDbSet<Product> Products { get; set; } IDbSet<Category> Categories { get; set; } int SaveChanges(); } public class DataContext : DbContext, IContext { public IDbSet<Product> Products { get; set; } public IDbSet<Category> Categories { get; set; } public DataContext(string connectionString) : base(connectionString) { } } 

服务

 public class ProductService : IProductService { private IContext _context; public ProductService(IContext dbContext) { _context = dbContext; } public IEnumerable<Product> GetAll() { var query = from p in _context.Products select p; return query; } } 

目前我正在做一些事情的心态:

  1. 用这种方法嘲讽EF上下文 – Mocking EF当unit testing或直接在像moq这样的接口上使用模拟框架时 – 考虑unit testing可能会通过的痛苦,但不一定是端到端的工作,并通过集成testing进行备份?
  2. 也许使用像努力模仿EF的东西 – 我从来没有使用它,不知道是否有其他人在野外使用它?
  3. 懒得testing任何简单地callback给EF的东西 – 所以基本上直接调用EF的服务方法(getAll等)不是经过unit testing,只是集成testing?

那里的人真的没有一个回购和成功吗?

这是我很感兴趣的一个话题。有许多纯粹主义者说你不应该testing像EF和NHibernate这样的技术。 他们是正确的,他们已经经过了非常严格的testing,而且之前的回答指出,花费大量的时间来testing你不拥有的东西往往毫无意义。

但是,您在下面拥有数据库! 这就是我认为这种方法崩溃的原因,您不需要testingEF / NH是否正确地完成了他们的工作。 你需要testing你的映射/实现是否与你的数据库一起工作。 在我看来,这是你可以testing的系统中最重要的部分之一。

严格地说,我们正在摆脱unit testing领域,进入集成testing领域,但是校长仍然是一样的。

你需要做的第一件事是能够模拟你的DAL,这样你的BLL可以独立于EF和SQL进行testing。 这些是你的unit testing。 接下来,您需要devise您的集成testing来certificate您的DAL,我认为这些都是重要的。

有几件事情需要考虑:

  1. 您的数据库需要在每个testing中处于已知状态。 大多数系统使用备份或为此创build脚本。
  2. 每个testing必须是可重复的
  3. 每个testing必须是primefaces的

有两种主要的设置数据库的方法,首先是运行一个UnitTest创build数据库脚本。 这可以确保您的unit testing数据库在每次testing开始时总是处于相同的状态(您可以重置或者在事务中运行每个testing来确保这一点)。

您的其他选项是我所做的,为每个单独的testing运行特定的设置。 我相信这是最好的方法,主要有两个原因:

  • 你的数据库比较简单,每个testing你都不需要一个完整的模式
  • 每个testing都更安全,如果您在创build脚本中更改了一个值,则不会使其他几十个testing失效。

不幸的是你的妥协是速度。 运行所有这些testing需要时间,以运行所有这些设置/拆卸脚本。

最后一点,编写如此大量的SQL来testing您的ORM可能非常困难。 这是我采取一个非常讨厌的方法(纯粹主义者在这里会不同意我)。 我用我的ORM来创build我的testing! 我的系统中每个DALtesting都没有单独的脚本,而是有一个testing设置阶段,它创build对象,将它们附加到上下文并保存。 然后我运行我的testing。

这远远不是理想的解决scheme,但实际上,我发现它很容易pipe理(特别是当你有几千个testing时),否则你会创build大量的脚本。 实用性超过纯度。

毫无疑问,我会在几年(几个月/几天)后回顾这个答案,并且不同意我的观点,但是这是我目前的做法。

总结一下我上面所说的一切,这是我典型的DB集成testing:

 [Test] public void LoadUser() { this.RunTest(session => // the NH/EF session to attach the objects to { var user = new UserAccount("Mr", "Joe", "Bloggs"); session.Save(user); return user.UserID; }, id => // the ID of the entity we need to load { var user = LoadMyUser(id); // load the entity Assert.AreEqual("Mr", user.Title); // test your properties Assert.AreEqual("Joe", user.Firstname); Assert.AreEqual("Bloggs", user.Lastname); } } 

这里要注意的关键是两个循环的会话是完全独立的。 在你的RunTest实现中,你必须确保上下文被提交和销毁,并且你的数据只能来自数据库的第二部分。

编辑13/10/2014

我曾经说过,我可能会在接下来的几个月里修改这个模型。 虽然我基本上支持我上面提出的方法,但我已经稍微更新了我的testing机制。 我现在倾向于在TestSetup和TestTearDown中创build实体。

 [SetUp] public void Setup() { this.SetupTest(session => // the NH/EF session to attach the objects to { var user = new UserAccount("Mr", "Joe", "Bloggs"); session.Save(user); this.UserID = user.UserID; }); } [TearDown] public void TearDown() { this.TearDownDatabase(); } 

然后分别testing每个属性

 [Test] public void TestTitle() { var user = LoadMyUser(this.UserID); // load the entity Assert.AreEqual("Mr", user.Title); } [Test] public void TestFirstname() { var user = LoadMyUser(this.UserID); Assert.AreEqual("Joe", user.Firstname); } [Test] public void TestLastname() { var user = LoadMyUser(this.UserID); Assert.AreEqual("Bloggs", user.Lastname); } 

这种方法有几个原因:

  • 没有额外的数据库调用(一个设置,一个拆卸)
  • testing更加细化,每个testingvalidation一个属性
  • Setup / TearDown逻辑从Test方法本身中删除

我觉得这使得testing类更简单,testing更细化( 单个断言是好的 )

编辑5/3/2015

这种方法的另一个修订。 尽pipe课程级别的设置对于加载属性等testing非常有帮助,但在需要不同设置的情况下,它们的用处不大。 在这种情况下,为每个案件设立一个新class级是矫枉过正的。

为了帮助我,现在我倾向于有两个基类SetupPerTestSingleSetup 。 这两个类根据需要公开框架。

SingleSetup我们有一个非常类似于我的第一个编辑中描述的机制。 一个例子是

 public TestProperties : SingleSetup { public int UserID {get;set;} public override DoSetup(ISession session) { var user = new User("Joe", "Bloggs"); session.Save(user); this.UserID = user.UserID; } [Test] public void TestLastname() { var user = LoadMyUser(this.UserID); // load the entity Assert.AreEqual("Bloggs", user.Lastname); } [Test] public void TestFirstname() { var user = LoadMyUser(this.UserID); Assert.AreEqual("Joe", user.Firstname); } } 

但是确保只加载正确实体的引用可以使用SetupPerTest方法

 public TestProperties : SetupPerTest { [Test] public void EnsureCorrectReferenceIsLoaded() { int friendID = 0; this.RunTest(session => { var user = CreateUserWithFriend(); session.Save(user); friendID = user.Friends.Single().FriendID; } () => { var user = GetUser(); Assert.AreEqual(friendID, user.Friends.Single().FriendID); }); } [Test] public void EnsureOnlyCorrectFriendsAreLoaded() { int userID = 0; this.RunTest(session => { var user = CreateUserWithFriends(2); var user2 = CreateUserWithFriends(5); session.Save(user); session.Save(user2); userID = user.UserID; } () => { var user = GetUser(userID); Assert.AreEqual(2, user.Friends.Count()); }); } } 

总之,这两种方法都取决于你试图testing的内容。

努力体验反馈在这里

在大量的阅读之后,我一直在使用Effort进行testing:在testing过程中,Context是由一个返回一个内存版本的工厂构build的,这个工厂可以让我每次testing一个空白的版本。 在testing之外,工厂解决了返回整个上下文的问题。

不过,我有一种感觉,即对数据库的全function模拟进行testing往往会拖拽testing; 你意识到你必须照顾build立一大堆的依赖,以testing系统的一部分。 你也倾向于组织一起testing,可能不相关,只是因为只有一个巨大的对象,处理所有事情。 如果你不注意,你可能会发现自己正在进行集成testing,而不是unit testing

我会喜欢testing更抽象的东西,而不是一个巨大的DBContext,但我无法find有意义的testing和裸骨testing之间的甜蜜点。 把它凑到我的经验不足。

所以我觉得努力有趣; 如果您需要在地面运行,这是快速入门并获得结果的好工具。 不过,我认为下一步应该是更加优雅和抽象的东西,接下来我将要进行调查。 collections此帖以查看下一个地方:)

编辑补充 :努力需要一些时间来预热,所以你在看约。 testing启动5秒钟。 如果你需要你的testing套件非常高效,这对你来说可能是个问题。


编辑澄清:

我用Effort来testing一个web服务应用程序。 每个进入的消息M通过Windsor路由到IHandlerOf<M> 。 Castle.Windsor解决了解决组件依赖关系的IHandlerOf<M>的问题。 其中一个依赖是DataContextFactory ,它允许处理程序请求工厂

在我的testing中,我直接实例化了IHandlerOf组件,模拟了SUT的所有子组件,并将Effort包装的DataContextFactory处理为处理程序。

这意味着我不是严格意义上的unit testing,因为数据库受到了我的testing。 不过,正如我上面所说的那样,让我在地面运行,我可以快速testing应用程序中的一些要点

如果你想单元testing代码,那么你需要从外部资源(例如数据库)中隔离你想testing的代码(在这种情况下是你的服务)。 你也许可以用某种内存中的EF提供程序来做这件事,然而更常见的方法是用某种types的存储库模式抽象出你的EF实现。 没有这种隔离,你写的任何testing都将是集成testing,而不是unit testing。

至于testingEF代码 – 我为自己的仓库编写了自动化集成testing,这些testing在初始化期间将各种行写入数据库,然后调用我的仓库实现以确保它们的行为与预期相同(例如,确保结果被正确过滤,或者他们按照正确的顺序sorting)。

这些是集成testing而不是unit testing,因为testing依赖于存在数据库连接,并且目标数据库已经安装了最新的最新模式。

我不会unit testing我不拥有的代码。 你在这里testing什么,MSFT编译器的作品?

也就是说,为了使这个代码可testing,您几乎必须将您的数据访问层与业务逻辑代码分开。 我所做的是把所有我的EF的东西,并把它放在一个(或多个)DAO或DAL类,也有相应的接口。 然后,我写我的服务将DAO或DAL对象注入作为依赖(build设者注射优选)引用作为接口。 现在需要testing的部分(代码)可以通过模拟DAO接口并将其注入到unit testing中的服务实例中进行testing。

 //this is testable just inject a mock of IProductDAO during unit testing public class ProductService : IProductService { private IProductDAO _productDAO; public ProductService(IProductDAO productDAO) { _productDAO = productDAO; } public List<Product> GetAllProducts() { return _productDAO.GetAll(); } ... } 

我会认为实时数据访问层是集成testing的一部分,而不是unit testing。 我曾经看过一些人对hibernate之前做了多less次访问进行validation,但是他们在一个涉及数十万条logging的项目中,这些额外的行程真的很重要。

为了达到这些考虑,我已经摸索过了。

1-如果我的应用程序访问数据库,为什么testing不应该? 如果数据访问有问题怎么办? testing必须事先知道,并提醒自己这个问题。

2-版本库模式有点困难和耗时。

所以我想出了这个办法,我觉得不是最好的,但是实现了我的期望:

 Use TransactionScope in the tests methods to avoid changes in the database. 

要做到这一点是必要的:

1-将EntityFramework安装到testing项目中。 2-将连接string放入Test Project的app.config文件中。 3-引用testing项目中的dll System.Transactions。

唯一的副作用是即使事务被中止,身份种子也会在尝试插入时增加。 但是由于testing是针对开发数据库进行的,所以这应该不成问题。

示例代码:

 [TestClass] public class NameValueTest { [TestMethod] public void Edit() { NameValueController controller = new NameValueController(); using(var ts = new TransactionScope()) { Assert.IsNotNull(controller.Edit(new Models.NameValue() { NameValueId = 1, name1 = "1", name2 = "2", name3 = "3", name4 = "4" })); //no complete, automatically abort //ts.Complete(); } } [TestMethod] public void Create() { NameValueController controller = new NameValueController(); using (var ts = new TransactionScope()) { Assert.IsNotNull(controller.Create(new Models.NameValue() { name1 = "1", name2 = "2", name3 = "3", name4 = "4" })); //no complete, automatically abort //ts.Complete(); } } } 

所以这里的东西,entity framework是一个实现,尽pipe它抽象了数据库交互的复杂性,直接交互仍然是紧耦合,这就是为什么testing混乱。

unit testing是关于testing一个函数的逻辑,并且每个潜在的结果都与任何外部依赖(在本例中是数据存储)隔离。 为了做到这一点,您需要能够控制数据存储的行为。 例如,如果你想声明你的函数返回false,如果提取的用户不符合一些条件,那么你的[mocked]数据存储应该被configuration为总是返回一个不符合条件的用户,相反的说法。

有了这个说法,并且接受EF是一个实现的事实,我可能会赞成提取一个仓库的想法。 看起来有点多余? 这不是,因为你正在解决一个隔离你的代码与数据实现的问题。

在DDD中,存储库只能返回聚合根,而不是DAO。 这样,存储库的使用者就不必知道数据的实现(因为它不应该),我们可以用它作为如何解决这个问题的一个例子。 在这种情况下,由EF生成的对象是一个DAO,因此应该隐藏您的应用程序。 您定义的存储库的另一个好处。 您可以将业务对象定义为其返回types,而不是EF对象。 现在,repo所做的就是隐藏对EF的调用,并将EF响应映射到repos签名中定义的业务对象。 现在,您可以使用该回购代替您注入到类中的DbContext依赖项,因此,现在您可以嘲笑该接口,为您提供所需的控制,以便单独testing您的代码。

这是一个更多的工作,许多人嗤之以鼻,但它解决了一个真正的问题。 有一个内存提供者被提到了一个不同的答案,可能是一个选项(我没有尝试过),它的存在certificate了实践的必要性。

我完全不同意这个最好的答案,因为它回避了隔离你的代码的真正问题,然后继续testing你的映射。 通过一切手段,如果你想testing你的映射,但在这里解决实际问题,并获得一些真正的代码覆盖率。

总之,我会说不,果汁是不值得挤压testing服务方法与检索模型数据的一条线。 根据我的经验,TDD的新手们都想testing一切。 将一个门面抽象为第三方框架的旧板栗,就可以创build一个模拟框架API来与你混合/扩展,这样就可以注入虚拟数据,这在我看来没有什么价值。 每个人对unit testing的最佳程度都有不同的看法。 我现在更倾向于务实,并问我自己,如果我的testing真的是增加了最终产品的价值,并且花了多less钱。

我想分享一个评论和简要讨论的方法,但是展示一个我目前用来帮助unit testing基于EF的服务的实例。

首先,我很乐意使用EF Core的内存提供程序,但这是关于EF 6.此外,对于像RavenDB这样的其他存储系统,我也会通过内存数据库提供者来进行testing。 再次 – 这是专门帮助testing基于EF的代码没有很多仪式

下面是我提出一个模式时的目标:

  • 团队中的其他开发人员必须很容易理解
  • 它必须尽可能地隔离EF代码
  • 它不能涉及到创build奇怪的多任务接口(如“通用”或“典型”存储库模式)
  • 在unit testing中configuration和设置必须很容易

我同意以前的表述,EF仍然是一个实现细节,可以感觉到需要抽象它来做一个“纯粹的”unit testing。 我也同意,理想情况下,我想确保EF代码本身的工作 – 但这涉及到沙箱数据库,内存提供商等。我的方法解决了这两个问题 – 你可以安全地unit testing与EF相关的代码, 创build集成testing专门testing您的EF代码。

我实现这一目标的方法是将EF代码简单地封装到专用的Query和Command类中。 这个想法很简单:只需将任何EF代码包装在一个类中,并依赖于最初使用它的类中的一个接口。 我需要解决的主要问题是避免在类中添加大量依赖,并在testing中设置大量代码。

这是一个有用的,简单的图书馆进来: Mediatr 。 它允许简单的进程内消息传递,并通过从执行代码的处理程序中分离“请求”来实现。 这有一个额外的好处,从“如何”解耦“什么”。 例如,通过将EF代码封装成小块,它可以让你用另一个提供者或完全不同的机制replace实现,因为你所做的只是发送一个请求来执行一个动作。

利用dependency injection(有或没有框架 – 您的偏好),我们可以轻松地模拟中介和控制请求/响应机制,以启用unit testingEF代码。

首先,假设我们有一个需要testing的业务逻辑的服务:

 public class FeatureService { private readonly IMediator _mediator; public FeatureService(IMediator mediator) { _mediator = mediator; } public async Task ComplexBusinessLogic() { // retrieve relevant objects var results = await _mediator.Send(new GetRelevantDbObjectsQuery()); // normally, this would have looked like... // var results = _myDbContext.DbObjects.Where(x => foo).ToList(); // perform business logic // ... } } 

你开始看到这种方法的好处吗? 您不仅可以明确地将所有与EF相关的代码封装到描述性类中,而且可以通过移除处理请求的“如何”的实现问题来允许扩展性 – 此类不关心相关对象是否来自EF,MongoDB,或一个文本文件。

现在请求和处理程序,通过MediatR:

 public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> { // no input needed for this particular request, // but you would simply add plain properties here if needed } public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> { private readonly IDbContext _db; public GetRelevantDbObjectsEFQueryHandler(IDbContext db) { _db = db; } public DbObject[] Handle(GetRelevantDbObjectsQuery message) { return _db.DbObjects.Where(foo => bar).ToList(); } } 

正如你所看到的,抽象是简单的,封装的。 这也是绝对可testing的,因为在一个集成testing中,你可以单独testing这个类 – 在这里没有混合的业务问题。

那么我们的function服务的unit testing是什么样的呢? 这很简单。 在这种情况下,我使用Moq做嘲弄(使用任何令你高兴的事情):

 [TestClass] public class FeatureServiceTests { // mock of Mediator to handle request/responses private Mock<IMediator> _mediator; // subject under test private FeatureService _sut; [TestInitialize] public void Setup() { // set up Mediator mock _mediator = new Mock<IMediator>(MockBehavior.Strict); // inject mock as dependency _sut = new FeatureService(_mediator.Object); } [TestCleanup] public void Teardown() { // ensure we have called or expected all calls to Mediator _mediator.VerifyAll(); } [TestMethod] public void ComplexBusinessLogic_Does_What_I_Expect() { var dbObjects = new List<DbObject>() { // set up any test objects new DbObject() { } }; // arrange // setup Mediator to return our fake objects when it receives a message to perform our query // in practice, I find it better to create an extension method that encapsulates this setup here _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback( (GetRelevantDbObjectsQuery message, CancellationToken token) => { // using Moq Callback functionality, you can make assertions // on expected request being passed in Assert.IsNotNull(message); }); // act _sut.ComplexBusinessLogic(); // assertions } } 

你可以看到我们所需要的只是一个单一的设置,我们甚至不需要configuration任何额外的东西 – 这是一个非常简单的unit testing。 让我们来清楚一点:完全可以做到没有像Mediatr这样的东西(你只需要实现一个接口,然后模拟它进行testing,例如IGetRelevantDbObjectsQuery ),但是实际上对于具有许多特性和查询/命令的大型代码库,我喜欢封装和Mediatr提供的先天DI支持。

如果你想知道如何组织这些类,这很简单:

 - MyProject - Features - MyFeature - Queries - Commands - Services - DependencyConfig.cs (Ninject feature modules) 

通过特征切片进行组织是非常重要的,但是这将所有相关/相关的代码保存在一起并且容易被发现。 最重要的是,我将查询和命令分开 – 按照命令/查询分离原则。

这符合我所有的标准:仪式低,很容易理解,还有额外隐藏的好处。 例如,你如何处理储蓄变化? 现在,您可以通过使用angular色接口( IUnitOfWork.SaveChangesAsync() )和模拟对单个angular色接口的调用来简化Db上下文,或者可以在RequestHandler中封装提交/回滚 – 但是您更喜欢这样做取决于您只要它是可维护的。 例如,我很想创build一个通用的请求/处理程序,你只需要传递一个EF对象,然后将其保存/更新/删除 – 但是你必须问你的意图是什么,记住如果你想swap out the handler with another storage provider/implementation, you should probably create explicit commands/queries that represent what you intend to do. More often than not, a single service or feature will need something specific–don't create generic stuff before you have a need for it.

There are of course caveats to this pattern–you can go too far with a simple pub/sub mechanism. I've limited my implementation to only abstracting EF-related code, but adventurous developers could start using MediatR to go overboard and message-ize everything–something good code review practices and peer reviews should catch. That's a process issue, not an issue with MediatR, so just be cognizant of how you're using this pattern.

You wanted a concrete example of how people are unit testing/mocking EF and this is an approach that's working successfully for us on our project–and the team is super happy with how easy it is to adopt. 我希望这有帮助! As with all things in programming, there are multiple approaches and it all depends on what you want to achieve. I value simplicity, ease of use, maintainability, and discoverability–and this solution meets all those demands.

I like to separate my filters from other portions of the code and test those as I outline on my blog here http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

That being said, the filter logic being tested is not identical to the filter logic executed when the program is run due to the translation between the LINQ expression and the underlying query language, such as T-SQL. Still, this allows me to validate the logic of the filter. I don't worry too much about the translations that happen and things such as case-sensitivity and null-handling until I test the integration between the layers.

There is Effort which is an in memory entity framework database provider. I've not actually tried it… Haa just spotted this was mentioned in the question!

Alternatively you could switch to EntityFrameworkCore which has an in memory database provider built-in.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

I used a factory to get a context, so i can create the context close to its use. This seems to work locally in visual studio but not on my TeamCity build server, not sure why yet.

 return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");