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

时间:2014-03-27 14:37:57

标签: c# entity-framework unit-testing entity-framework-6

我刚刚开始使用单元测试和TDD。我之前已涉足,但现在我决定将其添加到我的工作流程中并编写更好的软件。

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

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

我想这是一个问题,有没有意义这样做?如果是这样的话,人们如何在野外做这件事,因为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上下文 - Mocking EF When Unit Testing或直接在界面上使用模拟框架(如moq) - 承担单元测试可能通过的痛苦但不一定是端到端工作并支持它们集成测试?
  2. 也许使用类似Effort之类的东西来模拟EF - 我从来没有使用它,也不确定是否有其他人在野外使用它?
  3. 不用费力去测试任何只是回调EF的东西 - 所以基本上直接调用EF的服务方法(getAll等)不是单元测试的,而只是集成测试?
  4. 那里的任何人实际上没有回购并且成功了吗?

12 个答案:

答案 0 :(得分:165)

这是我非常感兴趣的话题。有许多纯粹主义者说你不应该测试EF和NHibernate等技术。他们是对的,他们已经进行了非常严格的测试,而且之前的回答表明,花大量的时间测试你不拥有的东西往往毫无意义。

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

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

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

有几件事需要考虑:

  1. 每次测试时,您的数据库都需要处于已知状态。大多数系统都使用备份或创建脚本。
  2. 每项测试必须是可重复的
  3. 每项测试必须是原子的
  4. 设置数据库有两种主要方法,第一种是运行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逻辑

    我觉得这会使测试类更简单,测试更精细(single asserts are good

    编辑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);
      }
    }
    

    但是,确保仅加载正确的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());
          });
       }
    }
    

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

答案 1 :(得分:20)

努力经验反馈

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

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

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

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

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


编辑澄清:

我使用Effort来测试一个webservice应用程序。输入的每条消息M都通过Windsor路由到IHandlerOf<M>。 Castle.Windsor解析IHandlerOf<M>,它可以重新组合组件的依赖关系。其中一个依赖项是DataContextFactory,它允许处理程序请求工厂

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

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

答案 2 :(得分:12)

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

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

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

答案 3 :(得分: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();
    }

    ...
}

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

答案 4 :(得分:7)

我在某个时候摸索了以达到这些考虑因素:

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

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

所以我想出了这种方法,我认为这不是最好的,但实现了我的期望:

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

要做到这一点是必要的:

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

答案 5 :(得分:7)

所以,实际上,Entity Framework是一个实现,所以尽管它抽象了数据库交互的复杂性,但是直接交互仍然是紧密的耦合,这就是为什么它会让人感到困惑的原因。测试

单元测试是关于测试函数的逻辑以及它的每个潜在结果,而不是任何外部依赖,在这种情况下,它是数据存储。为此,您需要能够控制数据存储的行为。例如,如果要断言如果获取的用户不满足某些条件,则函数返回false,则应将[mocked]数据存储配置为始终返回不符合条件的用户,对于相反的断言,反之亦然。

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

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

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

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

答案 6 :(得分:4)

简而言之,我会说不,使用单行检索模型数据来测试服务方法是不值得的。根据我的经验,初次接触TDD的人都想测试一切。将一个外观抽象到第三方框架的旧板栗只是为了让你可以创建一个模拟那个框架API,你可以用它来进行bastardise / extend以便你可以注入虚拟数据在我的脑海中没什么价值。每个人对单元测试的最佳程度有不同的看法。这些天我倾向于更务实,并问自己,我的测试是否真的为最终产品增加了价值,并且成本是多少。

答案 7 :(得分:3)

我想分享一个评论和简要讨论的方法,但展示了我目前用来帮助单元测试基于EF的服务的实际示例。

首先,我希望使用EF Core的内存提供程序,但这是关于EF 6.此外,对于像RavenDB这样的其他存储系统,我也是通过in-进行测试的支持者内存数据库提供者再次 - 这是专门帮助测试基于EF的代码没有很多仪式

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

  • 团队中的其他开发人员必须明白
  • 必须在最可能的级别隔离EF代码
  • 不得涉及创建奇怪的多责任界面(例如&#34;泛型&#34;或&#34;典型&#34;存储库模式)
  • 必须易于在单元测试中进行配置和设置

我同意先前的陈述,即EF仍然是一个实现细节,你可以觉得你需要抽象它以便做一个纯粹的&#34;单元测试。我也同意理想情况下,我希望确保EF代码本身有效 - 但这涉及沙盒数据库,内存提供程序等。我的方法解决了这两个问题 - 您可以安全地单元测试EF依赖代码创建集成测试以专门测试您的EF代码。

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

这是一个有用的简单库:Mediatr。它允许简单的进程内消息传递,并通过解耦&#34;请求&#34;来自实现代码的处理程序。这有一个额外的好处,即解耦&#34;什么&#34;来自&#34;怎么&#34;。例如,通过将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相关的代码封装到描述性类中,您还可以通过删除&#34;&#34;&#34;的实现问题来实现可扩展性。处理此请求 - 如果相关对象来自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这样的东西(你只需实现一个接口并模拟它进行测试,例如{{1} }),但在实践中,对于具有许多功能和查询/命令的大型代码库,我喜欢Mediatr提供的封装和固有的DI支持。

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

IGetRelevantDbObjectsQuery

Organizing by feature slices不是重点,但这会将所有相关/相关代码保持在一起并且很容易被发现。最重要的是,我按照Command/Query Separation原则将查询与命令分开。

这符合我的所有标准:它很低级,很容易理解,还有其他隐藏的好处。例如,您如何处理保存更改?现在,您可以通过使用角色接口(- MyProject - Features - MyFeature - Queries - Commands - Services - DependencyConfig.cs (Ninject feature modules) )和模拟对单个角色接口的调用来简化您的Db上下文,或者您可以在RequestHandler中封装提交/回滚 - 但是您更喜欢这样做取决于您,只要它是可维护的。例如,我很想创建一个通用的请求/处理程序,你只需要传递一个EF对象,然后保存/更新/删除它 - 但你必须问你的意图是什么,并记住,如果你想要将处理程序换成另一个存储提供程序/实现,您应该创建表示您打算执行的操作的显式命令/查询。通常情况下,单个服务或功能需要特定的东西 - 在您需要之前不要创建通用的东西。

当然警告这个模式 - 你可以用一个简单的pub / sub机制走得太远。我将我的实现仅限于抽象EF相关代码,但冒险的开发人员可以开始使用MediatR来过度使用并消息化所有内容 - 一些好的代码审查实践和同行评审应该抓住。这是一个过程问题,而不是MediatR的问题,所以只要知道你是如何使用这种模式的。

您想要一个关于人们如何进行单元测试/模拟EF的具体示例,这是一种在我们的项目中成功为我们工作的方法 - 团队非常满意采用它是多么容易。我希望这有帮助!与编程中的所有内容一样,有多种方法,这一切都取决于您想要实现的目标。我重视简单性,易用性,可维护性和可发现性 - 此解决方案满足所有这些要求。

答案 8 :(得分:3)

存在Effort,它是内存实体框架数据库提供程序。我实际上没有尝试过......哈哈刚发现问题中提到了这个问题!

或者,您可以切换到内置内存数据库提供程序的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;");

答案 9 :(得分:2)

我喜欢将我的过滤器与代码的其他部分分开,并按照我在博客上的大纲http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

进行测试

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

答案 10 :(得分:0)

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

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

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

答案 11 :(得分:0)

为了单元测试依赖于您的数据库的代码,您需要为每个测试设置一个数据库或模拟。

  1. 为您的所有测试拥有一个具有单一状态的数据库(真实的或模拟的)会很快咬住您;您无法测试所有记录是否有效,有些记录并非来自相同的数据。
  2. 在 OneTimeSetup 中设置内存数据库会出现问题,即在下一次测试开始之前旧数据库没有被清除。当您单独运行它们时,这将显示为测试有效,但在您运行它们时会失败。
  3. 理想情况下,单元测试应该只设置影响测试的内容

我在一个应用程序中工作,该应用程序有很多表,有很多连接和一些巨大的 Linq 块。这些需要测试。一个简单的分组被遗漏,或者一个导致超过 1 行的连接都会影响结果。

为了解决这个问题,我设置了一个繁重的单元测试助手,需要做很多工作,但使我们能够在任何状态下可靠地模拟数据库,并对 55 个互连表运行 48 个测试,以及整个数据库设置48次需要4.7秒。

方法如下:

  1. 在 Db 上下文类中确保每个表类都设置为虚拟

    public virtual DbSet<Branch> Branches { get; set; }
    public virtual DbSet<Warehouse> Warehouses { get; set; }
    
  2. 在 UnitTestHelper 类中创建一个方法来设置您的数据库。每个表类都是一个可选参数。如果没有提供,它将通过 Make 方法创建

    internal static Db Bootstrap(bool onlyMockPassedTables = false, List<Branch> branches = null, List<Products> products = null, List<Warehouses> warehouses = null)
    {
        if (onlyMockPassedTables == false) {
            branches ??= new List<Branch> { MakeBranch() };
            warehouses ??= new List<Warehouse>{ MakeWarehouse() };
        }
    
  3. 对于每个表类,其中的每个对象都映射到其他列表

        branches?.ForEach(b => {
            b.Warehouse = warehouses.FirstOrDefault(w => w.ID == b.WarehouseID);
        });
    
        warehouses?.ForEach(w => {
            w.Branches = branches.Where(b => b.WarehouseID == w.ID);
        });
    
  4. 并将其添加到 DbContext

         var context = new Db(new DbContextOptionsBuilder<Db>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
         context.Branches.AddRange(branches);
         context.Warehouses.AddRange(warehouses);
         context.SaveChanges();
         return context;
     }
    
  5. 定义一个 ID 列表,以便更容易重用它们并确保连接有效

     internal const int BranchID = 1;
     internal const int WarehouseID = 2;
    
  6. 为每个表创建一个 Make 以设置最基本的,但可以连接的版本

     internal static Branch MakeBranch(int id = BranchID, string code = "The branch", int warehouseId = WarehouseID) => new Branch { ID = id, Code = code, WarehouseID = warehouseId };
     internal static Warehouse MakeWarehouse(int id = WarehouseID, string code = "B", string name = "My Big Warehouse") => new Warehouse { ID = id, Code = code, Name = name };
    

工作量很大,但只需要做一次,然后您的测试就可以非常专注,因为数据库的其余部分都将为它设置。

[Test]
[TestCase(new string [] {"ABC", "DEF"}, "ABC", ExpectedResult = 1)]
[TestCase(new string [] {"ABC", "BCD"}, "BC", ExpectedResult = 2)]
[TestCase(new string [] {"ABC"}, "EF", ExpectedResult = 0)]
[TestCase(new string[] { "ABC", "DEF" }, "abc", ExpectedResult = 1)]
public int Given_SearchingForBranchByName_Then_ReturnCount(string[] codesInDatabase, string searchString)
{
    // Arrange
    var branches = codesInDatabase.Select(x => UnitTestHelpers.MakeBranch(code: $"qqqq{x}qqq")).ToList();
    var db = UnitTestHelpers.Bootstrap(branches: branches);
    var service = new BranchService(db);

    // Act
    var result = service.SearchByName(searchString);

    // Assert
    return result.Count();
}