人们应该如何使用Entity Framework 6进行单元测试?


170

我一般只是从单元测试和TDD开始。我以前涉猎过,但是现在我决心将其添加到我的工作流程中并编写更好的软件。

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

问题是我已经封锁了自己,因为我不想在存储库中抽象出EF(对于特定的查询,它仍然可以在服务之外使用),并且想测试我的服务(将使用EF Context) 。

我想这里是问题,这样做有什么意义吗?如果是这样的话,鉴于IQueryable造成的抽象漏洞以及Ladislav Mrnka关于单元测试的许多出色文章,人们会如何在野外进行操作,因为在处理内存中时Linq提供程序的不同实施到特定的数据库。

我要测试的代码看起来很简单。(这只是伪代码,试图了解我在做什么,我想使用TDD驱动创建)

语境

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上下文- 在单元测试时模拟EF或在像moq这样的接口上直接使用模拟框架-忍受单元测试可以通过但不一定能端到端地工作并通过集成测试进行备份的痛苦吗?
  2. 也许使用诸如Effort之类的东西来模拟EF-我从未使用过它,并且不确定是否有人在野外使用它?
  3. 不用费心测试任何简单地回调EF的东西-因此本质上直接调用EF的服务方法(getAll等)不是经过单元测试而是经过集成测试?

有人在没有回购协议且没有成功的情况下实际上在外面这样做吗?


嗨,莫迪卡,我最近在想这个问题(因为这个问题:stackoverflow.com/questions/25977388/…)在其中,我尝试更正式地描述我现在的工作方式,但是我很想听听你正在做。
萨米2014年

@samy,大家好,我们决定采用的方法不是直接测试任何直接接触EF的方法。对查询进行了测试,但将其作为集成测试而非单元测试。模拟EF感觉有点脏,但是这个项目很小,因此真正需要关注的是大量测试命中数据库对性能的影响,因此我们可以更加实际一些。我仍然不是100%地确定最好的方法是如何与您完全诚实,在某些时候,您将要使用EF(和您的数据库),而这里的单元测试并不适合我。
Modika 2014年

Answers:


186

这是我非常感兴趣的主题。许多纯粹主义者说,您不应该测试EF和NHibernate等技术。它们是正确的,它们已经过非常严格的测试,并且如先前的回答所述,花费大量时间测试您不拥有的东西通常毫无意义。

但是,您确实拥有数据库!在我看来,这就是这种方法失效的地方,您无需测试EF / NH是否能正确执行其工作。您需要测试您的映射/实现是否与数据库一起使用。在我看来,这是您可以测试的系统中最重要的部分之一。

但是严格来说,我们正在脱离单元测试的领域,而进入集成测试,但是原理保持不变。

您需要做的第一件事是能够模拟DAL,以便可以独立于EF和SQL来测试BLL。这些是您的单元测试。接下来,您需要设计集成测试以证明您的DAL,在我看来,这些都很重要。

有几件事情要考虑:

  1. 您的数据库在每次测试时都必须处于已知状态。大多数系统为此使用备份或创建脚本。
  2. 每个测试必须是可重复的
  3. 每个测试必须是原子的

设置数据库有两种主要方法,第一种是运行UnitTest创建数据库脚本。这样可确保您的单元测试数据库在每次测试开始时始终处于相同状态(您可以重置此状态或在事务中运行每个测试以确保这一点)。

您的另一选择是我要做的,为每个测试运行特定的设置。我认为这是最好的方法,主要有两个原因:

  • 您的数据库更简单,您不需要为每个测试使用完整的架构
  • 每个测试都比较安全,如果您在创建脚本中更改一个值,它不会使其他几十个测试无效。

不幸的是,您的妥协是速度。运行所有这些测试,运行所有这些设置/拆卸脚本需要花费时间。

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

这远不是理想的解决方案,但是在实践中,我发现这很容易管理(尤其是当您有数千个测试时),否则您将创建大量的脚本。实用胜过纯度。

毫无疑问,我将在几年(数月/日)后回头看一下这个答案,并且随着方法的改变,我也不同意我的看法,但这是我目前的方法。

为了总结以上我所说的一切,这是我典型的数据库集成测试:

[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

我确实说过,我可能会在接下来的几个月中修改此模型。虽然我在很大程度上支持我上面提倡的方法,但是我已经稍微更新了我的测试机制。我现在倾向于在TestSetup和TestTearDown中创建实体。

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

然后分别测试每个属性

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

这种方法有几个原因:

  • 没有其他数据库调用(一种设置,一种拆卸)
  • 测试更加精细,每个测试都验证一个属性
  • Setup / TearDown逻辑已从Test方法本身中删除

我觉得这使测试类更简单,测试更精细(单个断言是好的

编辑5/3/2015

关于此方法的另一种修订。虽然类级别的设置对于诸如加载属性之类的测试非常有帮助,但是在需要不同设置的情况下它们的用处就不大。在这种情况下,为每种情况设置一个新的类是过分的。

为了解决这个问题,我现在倾向于有两个基类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());
      });
   }
}

总之,这两种方法都可以工作,具体取决于您要测试的内容。


2
这是集成测试的另一种方法。TL; DR-使用应用程序本身设置测试数据,回滚每个测试的事务。
Gert Arnold

3
@Liath,反应很好。您已证实我对测试EF的怀疑。我的问题是这个;您的示例针对一个非常具体的案例,这很好。但是,正如您指出的那样,您可能需要测试数百个实体。遵循DRY原则(请勿重复自己),如何扩展解决方案,而不必每次都重复相同的基本代码模式?
Jeffrey A. Gochin

4
我不同意这一点,因为它完全避开了问题。单元测试是关于测试功能逻辑的。在OP示例中,逻辑依赖于数据存储。当您说不测试EF时,您是对的,但这不是问题。问题是与数据存储区隔离地测试您的代码。测试您的映射是一个完全不同的主题imo。为了测试逻辑是否与数据正确交互,您需要能够控制存储。
Sinaesthetic '16

7
关于是否应单独对Entity Framework进行单元测试,没有人碰壁。发生的事情是,您需要测试一些可以完成某些工作的方法,并且还需要对数据库进行EF调用。目的是模拟EF,以便您可以测试此方法而无需在构建服务器上使用数据库。
松饼人

4
我真的很喜欢这个旅程。感谢您随着时间的推移添加编辑内容-就像阅读源代码控制并了解您的思想如何发展一样。我也非常欣赏功能(带有EF)和单元(模拟EF)的区别。
汤姆·莱斯

21

努力经验反馈在这里

经过大量阅读后,我在测试中一直使用Effort:在测试过程中,Context由返回内存中版本的工厂构建,这使我每次都可以对空白板进行测试。在测试之外,将工厂解析为一个返回整个Context的工厂。

但是我有一种感觉,对数据库的全功能模拟进行测试会拖累测试;您意识到必须测试一系列依赖项才能测试系统的一部分。您也倾向于将可能不相关的测试组织在一起,只是因为只有一个巨大的对象可以处理所有事情。如果您不注意,则可能会发现自己在进行集成测试而不是单元测试

我本来希望针对更抽象的东西而不是庞大的DBContext进行测试,但是我找不到有意义的测试和裸露的测试之间的最佳结合点。总结一下我的经验。

所以我发现努力很有趣;如果您需要动手操作,它是快速入门并取得成果的好工具。但是,我认为下一步应该是更优雅,更抽象的东西,这就是我接下来要研究的内容。收藏此帖子,看看下一步如何发展:)

编辑添加:努力确实需要一些时间来热身,所以您正在寻找大约。测试启动5秒钟。如果您需要测试套件非常高效,这可能对您来说是个问题。


为澄清而编辑:

我用了努力来测试一个Web服务应用程序。输入的每个消息M都IHandlerOf<M>通过Windsor 路由到一个。Castle.Windsor解析,IHandlerOf<M>从而解决了组件的依赖关系。这些依赖项之一是DataContextFactory,它使处理程序可以要求工厂

在我的测试中,我直接实例化IHandlerOf组件,模拟SUT的所有子组件,并处理包装DataContextFactory到处理程序的工作量。

这意味着我不会严格意义上的单元测试,因为数据库会受到我的测试的打击。但是,正如我在上面说的那样,它可以让我踏上第一步,并且可以快速测试应用程序中的某些点


感谢您的投入,我要使该项目正常运行,可能要做的事情是从一些回购协议开始,看看我的进展如何,但是努力非常有趣。您对应用程序在哪一层一直不感兴趣?
莫迪卡

2
仅在努力已正确支持交易的情况下
Sedat Kapanoglu 2014年

当我们使用''而不是字符串中的null时,使用csv loader的字符串会产生麻烦。
山姆

13

如果要对代码进行单元测试,则需要将要测试的代码(在本例中为服务)与外部资源(例如数据库)隔离开。您可能可以使用某种内存中的EF提供程序来执行此操作,但是更常见的方法是抽象化EF实施,例如使用某种存储库模式。没有这种隔离,您编写的任何测试都将是集成测试,而不是单元测试。

至于测试EF代码-我为存储库编写了自动集成测试,这些存储库在初始化期间将各种行写入数据库,然后调用存储库实现以确保其行为符合预期(例如,确保正确过滤结果,或者它们以正确的顺序排序)。

这些是集成测试,而不是单元测试,因为测试依赖于存在数据库连接,并且目标数据库已经安装了最新的架构。


感谢@justin,我了解存储库模式,但是阅读诸如ayende.com/blog/4784/…lostechies.com/jimmybogard/2009/09/11/wither-the-repository等内容使我认为我不知道不需要这个抽象层,但是这些又再次谈论了一种非常令人困惑的Query方法。
莫迪卡2014年

7
@Modika Ayende选择了较差的存储库模式实施方式来进行评论,结果100%正确-它设计过度,没有任何好处。一个好的实现可以将代码的可单元测试部分与DAL实现隔离开来。直接使用NHibernate和EF会使代码难以(如果不是不可能的话)进行单元测试,并导致刚性的整体代码库。我仍然对存储库模式表示怀疑,但是我100%确信您需要以某种方式隔离DAL实现,而存储库是到目前为止我发现的最好的东西。
贾斯汀

2
@Modika再次阅读第二篇文章。他说的不是“我不要这个抽象层”。另外,请从Fowler(martinfowler.com/eaaCatalog/repository.html)或DDD(dddcommunity.org/resources/ddd_terms)中了解原始存储库模式。如果不完全理解原始概念,请不要相信反对者。他们真正批评的是最近滥用了模式,而不是模式本身(尽管他们可能不知道这一点)。
guillaume31 2014年

1
@ guillaume31我不反对存储库模式(我理解它),我只是想弄清楚是否需要它来抽象该级别的抽象,是否可以忽略它并通过模拟直接针对EF进行测试并在应用程序中更高层次的测试中使用它。此外,如果我不使用存储库,则可以使用EF扩展功能集,而使用存储库则可能无法获得。
莫迪卡

将DAL与存储库隔离后,我需要某种方式来“模拟”数据库(EF)。到目前为止,嘲弄上下文和各种异步扩展(ToListAsync(),FirstOrDefaultAsync()等)对我来说都是令人沮丧的。
凯文·伯顿

9

因此,实体框架是一个实现,因此尽管它抽象了数据库交互的复杂性,但是直接交互仍然紧密耦合,这就是测试令人困惑的原因。

单元测试是关于独立于任何外部依赖项(在此情况下为数据存储)测试功能的逻辑及其潜在结果的逻辑。为此,您需要能够控制数据存储的行为。例如,如果要断言如果获取的用户不符合某些条件,则函数返回false,则应将[模拟的]数据存储区配置为始终返回不符合条件的用户,反之反之亦然。

如此说来,并接受EF是实现的事实,我可能会赞成抽象存储库的想法。看起来有点多余?并非如此,因为您要解决的问题是将代码与数据实现隔离。

在DDD中,存储库仅返回聚合根,而不返回DAO。这样,存储库的使用者就不必知道数据的实现(因为它不应该这样),我们可以将其用作解决此问题的示例。在这种情况下,由EF生成的对象是DAO,因此应从您的应用程序中隐藏。您定义的存储库的另一个好处。您可以将业务对象定义为其返回类型,而不是EF对象。现在,回购所要做的就是隐藏对EF的调用,并将EF响应映射到回购签名中定义的业务对象。现在,您可以使用该存储库来代替您注入到类中的DbContext依赖项,因此,现在您可以模拟该接口,以提供所需的控件,以便隔离地测试代码。

这需要更多的工作,很多人对此表示赞同,但这解决了一个实际的问题。有一个在另一个答案中提到的内存提供程序可能是一个选择(我没有尝试过),它的存在本身就是需要这种做法的证据。

我完全不同意最高答案,因为它回避了隔离代码的实际问题,然后继续测试映射。如果需要,一定要测试您的映射,但是要在此处解决实际问题并获得一些实际的代码覆盖。


8

我不会对我不拥有的代码进行单元测试。您正在测试MSFT编译器在做什么?

就是说,要使该代码可测试,您几乎必须使数据访问层与业务逻辑代码分离。我要做的是将所有EF内容都放入一个(或多个)DAO或DAL类中,该类也具有相应的接口。然后,我编写我的服务,该服务将DAO或DAL对象作为依赖项(最好是构造函数注入)作为接口引用。现在,可以通过模拟DAO接口并将其注入到单元测试中的服务实例中来轻松地测试需要测试的部分(您的代码)。

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

    ...
}

我认为实时数据访问层是集成测试的一部分,而不是单元测试。我见过有人对冬眠数据库进行过多少次访问进行验证,但他们参与的项目涉及其数据存储中数十亿条记录,而这些额外的旅行确实很重要。


1
感谢您的回答,但是这与说您在此级别将EF内部隐藏在其中的信息库有什么区别?我真的不想抽象EF,尽管我可能仍在使用IContext接口执行此操作?我是这个新手,请客气:)
Modika 2014年

3
@Modika Repo也很好。无论您想要哪种模式。“我真的不想抽象EF”是否需要可测试的代码?
乔纳森·亨森

1
@Modika我的意思是,如果不分开关注点,您将没有任何可测试的代码。数据访问和业务逻辑必须位于不同的层中才能进行良好的可维护测试。
乔纳森·亨森

2
我只是觉得没有必要将EF包装在存储库抽象中,因为本质上IDbSet是回购协议,而上下文是UOW,我将对我的问题进行一些更新,因为这可能会引起误解。这个问题与任何抽象有关,重点是我要测试的是什么,因为我的查询不会在相同的边界(linq-to-entities与linq-to-objects)中运行,所以如果我只是在测试我的服务可以电话似乎有点浪费,或者我在这里过得还好吗?
莫迪卡2014年

1
,虽然我同意您的一般观点,但DbContext是工作的一个单元,IDbSets绝对是用于存储库实现的一些单元,而我并不是唯一想到这一点的人。我可以模拟EF,并且在某些层次上我需要运行集成测试,这是真的很重要吗(如果我是在存储库中还是在服务中进行)?与数据库紧密耦合并不是真正的问题,我敢肯定它会发生,但是我不会为可能不会发生的事情做计划。
莫迪卡

8

我摸索了一段时间以达到以下考虑:

1-如果我的应用程序访问数据库,为什么不应该进行测试?如果数据访问出现问题怎么办?测试必须事先知道并提醒自己有关问题。

2-存储库模式有些困难且耗时。

因此,我想到了这种方法,虽然我认为这不是最好的方法,但是却达到了我的期望:

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

为此,有必要:

1-将EntityFramework安装到测试项目中。2-将连接字符串放入Test Project的app.config文件中。3-在测试项目中引用dll System.Transactions。

唯一的副作用是,即使尝试中止事务,身份种子也会在尝试插入时增加。但是由于测试是针对开发数据库进行的,因此应该没有问题。

样例代码:

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

1
实际上,我非常喜欢这种解决方案。实施起来超级简单,测试场景更加真实。谢谢!
slopapa

1
在EF 6中,您将使用DbContext.Database.BeginTransaction,不是吗?
SwissCoder

5

简而言之,我想说不行,用一条检索模型数据的行来测试一种服务方法并不值得。以我的经验,刚接触TDD的人想测试所有东西。将外观抽象到第3方框架的旧方法只是为了让您可以创建该框架API的模拟模型,并使用它进行混蛋/扩展,以便您可以注入虚拟数据,这对我而言意义不大。每个人对最佳的单元测试有不同的看法。这些天,我倾向于更加务实,问自己,我的测试是否真的在为最终产品增加价值,以及增加了多少成本。


1
是的,实用主义。我仍然认为单元测试的质量不如原始代码的质量。当然,使用TDD可以改善您的编码实践并增强可维护性,这是有价值的,但是TDD的价值会逐渐降低。我们对数据库运行所有测试,因为它使我们确信我们对EF和表本身的使用是正确的。这些测试确实需要更长的时间才能运行,但是更加可靠。
野蛮人

3

我想分享一种经过评论和简短讨论的方法,但展示一个当前正在使用的实际示例,以帮助对基于EF的服务进行单元测试

首先,我很想使用EF Core的内存提供程序,但这是关于EF 6的。此外,对于其他存储系统(如RavenDB),我也支持通过内存数据库提供程序进行测试。再次重申-这是专门用于帮助测试基于EF的代码而无需进行过多的仪式

这是我提出模式时的目标:

  • 团队中的其他开发人员必须很容易理解
  • 它必须在尽可能高的级别上隔离EF代码
  • 它一定不能涉及创建怪异的多职责接口(例如“通用”或“典型”存储库模式)
  • 在单元测试中必须易于配置和设置

我同意前面的说法,即EF仍然是实现细节,可以感觉到需要抽象它才能进行“纯”单元测试。我也同意,理想情况下,我想确保EF代码本身可以工作-但这涉及沙箱数据库,内存提供程序等。我的方法解决了这两个问题-您可以安全地对与EF相关的代码进行单元测试创建集成测试以专门测试您的EF代码。

我做到这一点的方法是将EF代码简单地封装到专用的Query和Command类中。这个想法很简单:只需将任何EF代码包装在一个类中,并依赖本来会使用它的类中的接口。我需要解决的主要问题是避免在类中添加大量依赖项,并在测试中设置大量代码。

这是一个有用的简单库所在的位置:Mediatr。它允许进行简单的进程内消息传递,并通过将“请求”与实现代码的处理程序解耦来实现。这具有将“什么”与“如何”解耦的附加好处。例如,通过将EF代码封装为小块,可以使您用另一个提供程序或完全不同的机制替换实现,因为您所做的只是发送执行操作的请求。

利用依赖项注入(带有或不带有框架-您的偏好),我们可以轻松模拟中介程序并控制请求/响应机制,以启用EF代码的单元测试。

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

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

如您所见,抽象很简单并且被封装。它也是绝对可测试的,因为在集成测试中,您可以单独测试该类-这里没有混合的业务问题。

那么我们的功能服务的单元测试是什么样的?很简单。在这种情况下,我使用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
  }

}

您可以看到我们需要的只是一个设置,我们甚至不需要配置任何其他内容-这是一个非常简单的单元测试。让我们清楚一点:如果没有 Mediatr之类的东西(您可以简单地实现一个接口并对其进行模拟,以进行测试,例如IGetRelevantDbObjectsQuery),这是完全可能的,但是实际上对于具有许多功能和查询/命令的大型代码库,我喜欢封装和固有的DI支持Mediatr提供。

如果您想知道如何组织这些课程,那很简单:

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

通过特征切片进行组织并不是重点,但是这可以使所有相关/相关代码保持在一起并且易于发现。最重要的是,我遵循命令/查询分离原则将查询与命令分开。

这符合我的所有条件:礼节低,易于理解,并且还有其他隐藏的好处。例如,如何处理保存的更改?现在,您可以使用角色界面来简化Db上下文(IUnitOfWork.SaveChangesAsync())并模拟对单个角色接口的调用,或者您可以将提交/回滚封装在RequestHandlers中-但是,只要它是可维护的,则您更愿意这样做。例如,我很想创建一个通用的请求/处理程序,您只需在其中传递一个EF对象,它就会保存/更新/删除它-但您必须询问您的意图并记住是否要用另一个存储提供程序/实现换出处理程序,您可能应该创建表示您打算执行的显式命令/查询。通常,单个服务或功能将需要特定的东西-在需要之前不要创建通用的东西。

当然,这种模式有一些警告-您可以通过简单的pub / sub机制来解决问题。我将实现方式仅限于仅提取与EF相关的代码,但是喜欢冒险的开发人员可以开始使用MediatR过度介绍并消息化所有内容-良好的代码审阅实践和同行审阅应该引起人们的注意。这是一个过程问题,而不是MediatR的问题,因此只需了解您如何使用此模式即可。

您需要一个具体的示例来说明人们如何进行EF的单元测试/模拟,这是一种在我们的项目中对我们成功运行的方法-团队对采用它的简易性感到非常满意。我希望这有帮助!就像编程中的所有事物一样,有多种方法,这都取决于您要实现的目标。我重视简单性,易用性,可维护性和可发现性-该解决方案可以满足所有这些需求。


感谢您的回答,它很好地描述了使用Mediator的QueryObject模式,并且我也开始在项目中进行推广。我可能不得不更新问题,但是我不再进行EF单元测试了,抽象性太漏了(尽管SqlLite可能还可以),所以我只是对我查询数据库的东西进行集成测试,并对业务规则和其他逻辑进行单元测试。
莫迪卡'17

3

有一个工作在内存中的实体框架数据库提供程序。我实际上并没有尝试过...哈哈发现问题中提到了这一点!

或者,您可以切换到EntityFrameworkCore,它具有内置的内存数据库提供程序。

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

https://github.com/tamasflamich/effort

我使用了一个工厂来获取上下文,因此可以创建与其使用接近的上下文。这似乎可以在Visual Studio中本地运行,但不能在我的TeamCity构建服务器上运行,不确定为什么。

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

嗨,安德鲁,问题从来没有得到上下文,您可以将我们正在做的上下文工厂化,抽象出上下文并由工厂构建。最大的问题是内存和Linq4Entities的一致性,它们不一致,可能导致误导性测试。当前,我们只是集成测试数据库的内容,可能并不是每个人都介意的最佳过程。
莫迪卡

如果您有要模拟的上下文,则此Moq帮助程序可以正常工作(codeproject.com/Tips/1045590/…)。如果使用列表支持模拟的上下文,则其行为可能不像sql数据库支持的上下文那样。
安德鲁·帕特

2

我喜欢将过滤器与代码的其他部分分开,并按照我在博客中的概述进行测试,网址为http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

就是说,由于LINQ表达式和底层查询语言(例如T-SQL)之间的转换,因此正在测试的过滤器逻辑与运行程序时执行的过滤器逻辑不同。尽管如此,这仍使我可以验证过滤器的逻辑。在测试层之间的集成之前,我不必担心发生的翻译以及诸如区分大小写和空值处理之类的事情。


0

测试您期望实体框架做什么(即验证您的期望)很重要。我成功使用的一种方法是使用moq,如本示例所示(很长时间才能复制到此答案中):

https://docs.microsoft.com/zh-cn/ef/ef6/fundamentals/testing/mocking

但是请注意... SQL上下文不能保证按特定顺序返回事物,除非您在linq查询中有适当的“ OrderBy”,所以当您使用内存列表进行测试时,可能会编写通过的事物( linq-to-entities),但在使用(linq-to-sql)的uat / live环境中失败。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.