如果您打扰,人们如何使用Entity Framework 6进行单元测试?

Mod*_*ika 164 c# unit-testing entity-framework entity-framework-6

我刚刚开始使用单元测试和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)
    {

    }
}
Run Code Online (Sandbox Code Playgroud)

服务

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

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

  1. 用这样的方法模拟EF上下文 - 模拟EF当单元测试或直接在界面上使用模拟框架如moq时 - 考虑单元测试可能通过的痛苦但不一定端到端工作并用集成测试支持它们?
  2. 也许使用像Effort这样的东西来模拟EF - 我从来没有使用它,也不确定是否还有其他人在野外使用它?
  3. 没有费心去测试任何简单回调EF的东西 - 所以基本上直接调用EF的服务方法(getAll等)不是单元测试的,而只是集成测试?

那里的任何人实际上没有回购和成功吗?

Lia*_*ath 178

这是我非常感兴趣的话题.有许多纯粹主义者说你不应该测试EF和NHibernate等技术.他们是正确的,他们已经经过了严格的测试,并且作为之前的回答表明,花费大量时间测试你不拥有的东西通常是没有意义的.

但是,您拥有下面的数据库!这是我认为这种方法失败的地方,你不需要测试EF/NH是否正确地完成了他们的工作.您需要测试您的映射/实现是否正在使用您的数据库.在我看来,这是您可以测试的系统中最重要的部分之一.

严格来说,我们正在逐步退出单元测试领域并进入集成测试,但主体保持不变.

您需要做的第一件事是能够模拟您的DAL,以便您的BLL可以独立于EF和SQL进行测试.这些是你的单元测试.接下来,您需要设计集成测试来证明您的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);
  }
}
Run Code Online (Sandbox Code Playgroud)

这里要注意的关键是两个循环的会话是完全独立的.在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();
}
Run Code Online (Sandbox Code Playgroud)

然后单独测试每个属性

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

这种方法有几个原因:

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

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

编辑2015年5月3日

这种方法的另一个修订.虽然类级别设置对于诸如加载属性之类的测试非常有用,但在需要不同设置的情况下它们不太有用.在这种情况下,为每个案例设置一个新类是过度的.

为了帮助我,我现在倾向于有两个基类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);
  }
}
Run Code Online (Sandbox Code Playgroud)

但是,确保仅加载正确的entites的引用可以使用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());
      });
   }
}
Run Code Online (Sandbox Code Playgroud)

总之,这两种方法都取决于您要测试的内容.

  • 没有人关于你是否应该单独测试实体框架.会发生什么是你需要测试一些做某些事情的方法,并且碰巧对数据库进行EF调用.目标是模拟EF,以便您可以在构建服务器上不需要数据库的情况下测试此方法. (6认同)
  • 我不同意这一点,因为它完全避开了问题。单元测试是关于测试功能逻辑的。在OP示例中,逻辑依赖于数据存储。当您说不测试EF时,您是对的,但这不是问题。问题是与数据存储区隔离地测试您的代码。测试您的映射是一个完全不同的主题imo。为了测试逻辑是否与数据正确交互,您需要能够控制存储。 (4认同)
  • @Liath,很棒的回应.你已经证实了我对测试EF的怀疑.我的问题是这个; 你的例子是一个非常具体的案例,这很好.但是,正如您所指出的,您可能需要测试数百个实体.为了与DRY原则保持一致(不要重复自己),您如何扩展解决方案,而不是每次都重复相同的基本代码模式? (3认同)
  • 我真的很喜欢这个旅程.感谢您随着时间的推移添加编辑 - 这就像阅读源代码控制并了解您的思维如何演变.我非常欣赏功能(使用EF)和单位(模拟EF)的区别. (3认同)
  • [这里](http://stackoverflow.com/a/26267046/861716)一种不同的集成测试方法.TL; DR - 使用应用程序本身设置测试数据,每次测试回滚事务. (2认同)

sam*_*amy 20

努力经验反馈在这里

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

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

我本来希望测试一些更抽象的东西,而不是一个巨大的DBContext,但我找不到有意义的测试和裸骨测试之间的最佳点.把它归结为我的经验不足.

所以我发现Effort很有意思; 如果你需要开始运行它是一个快速入门并获得结果的好工具.然而,我认为下一步应该是更优雅和抽象的东西,这就是我接下来要调查的内容.收藏这篇文章,看看它下一步:)

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


编辑澄清:

我用Effort测试了一个webservice应用程序.输入的每条消息M被路由到IHandlerOf<M>via Windsor.Castle.Windsor解析了IHandlerOf<M>resovles组件的依赖关系.其中一个依赖项是DataContextFactory,它允许处理程序请求工厂

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

这意味着我没有严格意义上的单元测试,因为数据库受到我的测试的影响.然而,正如我上面所说,它让我开始运行,我可以快速测试应用程序中的一些点

  • 只有当Effort正确支持交易时 (2认同)

Jus*_*tin 13

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

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

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

  • @Modika Ayende选择了一个糟糕的存储库模式实现来批评,因此100%正确 - 它过度设计,并没有提供任何好处.一个好的实现将代码的单元可测试部分与DAL实现隔离开来.使用NHibernate和EF直接使代码难以(如果不是不可能)进行单元测试并导致严格的单片代码库.我仍然对存储库模式持怀疑态度,但是我100%确信你需要以某种方式隔离你的DAL实现,而存储库是我迄今为止发现的最好的东西. (6认同)
  • @Modika再次阅读第二篇文章。他说的不是“我不要这个抽象层”。另外,请从Fowler(http://martinfowler.com/eaaCatalog/repository.html)或DDD(http://dddcommunity.org/resources/ddd_terms/)中了解原始存储库模式。如果不完全理解原始概念,请不要相信反对者。他们真正批评的是最近滥用了模式,而不是模式本身(尽管他们可能不知道这一点)。 (2认同)
  • @guillaume31我并不反对存储库模式(我确实理解它),我只是想弄清楚我是否需​​要它来抽象该级别上已经存在的抽象,以及我是否可以省略它并通过模拟直接针对 EF 进行测试并在我的应用程序更高层的测试中使用它。此外,如果我不使用存储库,我将获得 EF 扩展功能集的好处,而使用存储库我可能无法获得这些好处。 (2认同)

Mar*_*eli 8

为了达到以下考虑,我偶尔会摸索:

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

2-存储库模式有点困难和耗时.

所以我提出了这种方法,我认为这不是最好的,但满足了我的期望:

Use TransactionScope in the tests methods to avoid changes in the database.
Run Code Online (Sandbox Code Playgroud)

为此,有必要:

1-将EntityFramework安装到Test Project中.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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 实际上,我非常喜欢这个解决方案。超级简单的实现和更真实的测试场景。谢谢! (2认同)

Sin*_*tic 8

所以这就是实体框架,实体框架是一个实现,所以尽管它抽象了数据库交互的复杂性,但是直接交互仍然是紧密耦合,这就是为什么它会让测试变得混乱.

单元测试是关于测试函数的逻辑,并且每个函数的潜在结果都与任何外部依赖关系隔离,在本例中,这些依赖关系是数据存储.为此,您需要能够控制数据存储的行为.例如,如果要断言如果获取的用户不满足某些条件,则函数返回false,则应将[mocked]数据存储配置为始终返回不符合条件的用户,并且反之亦然.

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

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

这是一个更多的工作,许多人嗤之以鼻,但它解决了一个真正的问题.在一个不同的答案中提到了一个内存提供者可能是一个选项(我还没有尝试过),它的存在是证明需要这种做法的证据.

我完全不同意最佳答案,因为它避开了真正的问题,即隔离你的代码然后继续测试你的映射.如果您愿意,一定要测试您的映射,但是在这里解决实际问题并获得一些真正的代码覆盖率.


Jon*_*son 7

我不会单元测试我不拥有的代码.你在这里测试的是,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();
    }

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

我认为实时数据访问层是集成测试的一部分,而不是单元测试.我见过人们对hibernate之前数据库的访问次数进行了验证,但他们在一个项目中涉及数据库中数十亿条记录,这些额外的行程确实很重要.

  • @Modika A Repo也很好.无论你想要什么样的模式."我真的不想抽象EF"你想要可测试的代码吗? (3认同)
  • 我只是觉得将EF封装在存储库抽象中并不感兴趣,因为本质上IDbSets是repo和UOW的上下文,我会更新我的问题,因为这可能会产生误导.问题来自任何抽象,主要的一点是我测试的是什么,因为我的queiries不会在相同的边界(linq-to-entities vs linq-to-objects)中运行所以如果我只测试我的服务是打电话似乎有点浪费或我在这里好吗? (2认同)
  • ,虽然我同意你的一般观点,但 DbContext 是一个工作单元,IDbSets 绝对是存储库实现的一部分,而且我不是唯一一个这么认为的人。我可以模拟 EF,并且在某些层我需要运行集成测试,如果我在存储库中还是在服务中进一步执行,这真的很重要吗?与数据库紧密耦合并不是真正的问题,我确信它会发生,但我不会为可能不会发生的事情做好计划。 (2认同)

Com*_*eIn 5

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


kam*_*cus 5

我想分享一种评论和简要讨论的方法,但展示我当前用于帮助对基于 EF 的服务进行单元测试的实际示例。

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

以下是我提出模式时的目标:

  • 对于团队中的其他开发人员来说必须简单易懂
  • 它必须在尽可能最低的级别隔离 EF 代码
  • 它不能涉及创建奇怪的多职责接口(例如“通用”或“典型”存储库模式)
  • 在单元测试中必须易于配置和设置

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

我实现这一点的方法是通过简单地封装 EF 代码到专用的查询和命令类中。这个想法很简单:只需将任何 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
    // ...    
  }
}
Run Code Online (Sandbox Code Playgroud)

您开始看到这种方法的好处了吗?你不仅明确封装到描述性类中,还通过消除“如何”处理此请求的实现问题来允许可扩展性 - 该类不关心相关对象是否来自 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();
  }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,抽象是简单且封装的。它也是绝对可测试的,因为在集成测试中,您可以单独测试此类——这里没有混合任何业务问题。

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

}
Run Code Online (Sandbox Code Playgroud)

您可以看到我们所需要的只是一个设置,我们甚至不需要配置任何额外的东西——这是一个非常简单的单元测试。让我们明确一点:如果没有像 Mediatr 这样的东西,这是完全可能的(您只需实现一个接口并模拟它进行测试,例如IGetRelevantDbObjectsQuery),但在实践中,对于具有许多功能和查询/命令的大型代码库,我喜欢封装和Mediatr 提供固有的 DI 支持。

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

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)
Run Code Online (Sandbox Code Playgroud)

按功能切片进行组织不是重点,但这将所有相关/依赖的代码放在一起并且易于发现。最重要的是,我遵循命令/查询分离原则将查询与命令分开。

这符合我的所有标准:简单、易于理解,而且还有额外的隐藏好处。例如,您如何处理保存更改?现在,您可以通过使用角色接口 ( IUnitOfWork.SaveChangesAsync()) 和对单个角色接口的模拟调用来简化您的数据库上下文,或者您可以将提交/回滚封装在您的 RequestHandler 中——但是您更喜欢这样做取决于您,只要它是可维护。例如,我很想创建一个通用请求/处理程序,您只需传递一个 EF 对象,它就会保存/更新/删除它——但您必须询问您的意图是什么,并记住,如果您想要用另一个存储提供程序/实现替换处理程序,您可能应该创建表示您打算执行的操作的显式命令/查询。通常,单个服务或功能需要特定的东西——在需要之前不要创建通用的东西。

当然,这种模式有一些警告——简单的发布/订阅机制可能会走得太远。我将我的实现限制为仅抽象与 EF 相关的代码,但是富有冒险精神的开发人员可以开始使用 MediatR 来过度使用 MediatR 并消息化所有内容 - 良好的代码审查实践和同行审查应该抓住这一点。这是一个流程问题,而不是 MediatR 的问题,因此只需了解如何使用此模式即可。

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