人们如何使用 Entity Framework 6 进行单元测试,你应该打扰吗? [英] How are people unit testing with Entity Framework 6, should you bother?

查看:28
本文介绍了人们如何使用 Entity Framework 6 进行单元测试,你应该打扰吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am just starting out with Unit testings and TDD in general. I have dabbled before but now I am determined to add it to my workflow and write better software.

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

I asked a question yesterday that kind of included this, but it seems to be a question on its own. I have sat down to start implementing a service class that I will use to abstract away the business logic from the controllers and map to specific models and data interactions using EF6.

问题是我已经给自己设置了障碍,因为我不想将 EF 抽象到存储库中(它仍然可以在服务之外用于特定查询等)并且想测试我的服务(EF 上下文将使用).

The issue is I have roadblocked myself already because I didn't want to abstract EF away in a repository (it will still be available outside the services for specific queries, etc) and would like to test my services (EF Context will be used).

我想问题是,这样做有什么意义吗?如果是这样,鉴于 IQueryable 引起的抽象泄漏以及 Ladislav Mrnka 的许多精彩帖子,人们是如何在野外做这件事的 关于单元测试的主题并不简单,因为在使用与特定数据库相关的内存实现时,Linq 提供程序存在差异.

Here I guess is the question, is there a point to doing this? If so, how are people doing it in the wild in light of the leaky abstractions caused by IQueryable and the many great posts by Ladislav Mrnka on the subject of unit testing not being straightforward because of the differences in Linq providers when working with an in memory implementation as apposed to a specific database.

我要测试的代码看起来很简单.(这只是尝试理解我在做什么的虚拟代码,我想使用 TDD 驱动创建)

The code I want to test seems pretty simple. (this is just dummy code to try and understand what i am doing, I want to drive the creation using 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;
    }
}

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

Currently I am in the mindset of doing a few things:

  1. 用类似这种方法模拟 EF 上下文 - 单元测试时模拟 EF 或直接在界面上使用模拟框架,如 moq - 忍受单元测试可能通过但不一定工作结束的痛苦用集成测试结束并支持它们?
  2. 也许使用诸如 Effort 之类的东西来模拟 EF - 我从未使用过它,不确定是否还有其他人在野外使用它?
  3. 不需要测试任何简单地回调到 EF 的东西——所以本质上直接调用 EF 的服务方法(getAll 等)没有经过单元测试,而只是经过集成测试?
  1. Mocking EF Context with something like this approach- Mocking EF When Unit Testing or directly using a mocking framework on the interface like moq - taking the pain that the unit tests may pass but not necessarily work end to end and back them up with Integration tests?
  2. Maybe using something like Effort to mock EF - I have never used it and not sure if anyone else is using it in the wild?
  3. Not bother testing anything that simply calls back to EF - so essentially service methods that call EF directly (getAll etc) are not unit tested but just integration tested?

有没有人在没有 Repo 的情况下真正做到这一点并取得了成功?

Anyone out there actually doing this out there without a Repo and having success?

推荐答案

这是我很感兴趣的一个话题.有很多纯粹主义者说你不应该测试像 EF 和 NHibernate 这样的技术.他们是对的,他们已经经过了非常严格的测试,而且正如之前的回答所说,花费大量时间测试您不拥有的东西通常毫无意义.

This is a topic I'm very interested in. There are many purists who say that you shouldn't test technologies such as EF and NHibernate. They are right, they're already very stringently tested and as a previous answer stated it's often pointless to spend vast amounts of time testing what you don't own.

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

However, you do own the database underneath! This is where this approach in my opinion breaks down, you don't need to test that EF/NH are doing their jobs correctly. You need to test that your mappings/implementations are working with your database. In my opinion this is one of the most important parts of a system you can test.

严格来说,我们正在从单元测试领域转移到集成测试领域,但原则保持不变.

Strictly speaking however we're moving out of the domain of unit testing and into integration testing but the principles remain the same.

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

The first thing you need to do is to be able to mock your DAL so your BLL can be tested independently of EF and SQL. These are your unit tests. Next you need to design your Integration Tests to prove your DAL, in my opinion these are every bit as important.

需要考虑以下几点:

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

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

There are two main approaches to setting up your database, the first is to run a UnitTest create DB script. This ensures that your unit test database will always be in the same state at the beginning of each test (you may either reset this or run each test in a transaction to ensure this).

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

Your other option is what I do, run specific setups for each individual test. I believe this is the best approach for two main reasons:

  • 您的数据库更简单,您不需要为每个测试使用完整的架构
  • 每个测试都更安全,如果您更改创建脚本中的一个值,它不会使许多其他测试失效.

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

Unfortunately your compromise here is speed. It takes time to run all these tests, to run all these setup/tear down scripts.

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

One final point, it can be very hard work to write such a large amount of SQL to test your ORM. This is where I take a very nasty approach (the purists here will disagree with me). I use my ORM to create my test! Rather than having a separate script for every DAL test in my system I have a test setup phase which creates the objects, attaches them to the context and saves them. I then run my test.

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

This is far from the ideal solution however in practice I find it's a LOT easier to manage (especially when you have several thousand tests), otherwise you're creating massive numbers of scripts. Practicality over purity.

毫无疑问,我会在几年(几个月/几天)后回顾这个答案,并且不同意我自己的方法,因为我的方法发生了变化 - 但这是我目前的方法.

I will no doubt look back at this answer in a few years (months/days) and disagree with myself as my approaches have changed - however this is my current approach.

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

To try and sum up everything I've said above this is my typical DB integration test:

[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 实现中,您必须确保上下文已提交和销毁,并且您的数据只能来自第二部分的数据库.

The key thing to notice here is that the sessions of the two loops are completely independent. In your implementation of RunTest you must ensure that the context is committed and destroyed and your data can only come from your database for the second part.

编辑 13/10/2014

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

I did say that I'd probably revise this model over the upcoming months. While I largely stand by the approach I advocated above I've updated my testing mechanism slightly. I now tend to create the entities in in the TestSetup and 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);
}

采用这种方法有几个原因:

There are several reasons for this approach:

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

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

I feel this makes the test class simpler and the tests more granular (single asserts are good)

编辑 5/3/2015

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

Another revision on this approach. While class level setups are very helpful for tests such as loading properties they are less useful where the different setups are required. In this case setting up a new class for each case is overkill.

为了帮助解决这个问题,我现在倾向于使用两个基类 SetupPerTestSingleSetup.这两个类根据需要公开框架.

To help with this I now tend to have two base classes SetupPerTest and SingleSetup. These two classes expose the framework as required.

SingleSetup 中,我们有一个与我在第一次编辑中描述的非常相似的机制.一个例子是

In the SingleSetup we have a very similar mechanism as described in my first edit. An example would be

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 方法

However references which ensure that only the correct entites are loaded may use a SetupPerTest approach

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

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

In summary both approaches work depending on what you are trying to test.

这篇关于人们如何使用 Entity Framework 6 进行单元测试,你应该打扰吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆