c# - 人们如何使用 Entity Framework 6 进行单元测试,你应该打扰吗?

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

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

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

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

我想这里有一个问题,这样做有什么意义吗?如果是这样,鉴于 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;
    }
}

目前我的心态是做一些事情:
  • 用类似这种方法模拟 EF 上下文-Mocking EF When Unit Testing或者直接在像 moq 这样的界面上使用模拟框架 - 忍受单元测试可能通过但不一定端到端工作并用集成测试支持它们的痛苦?
  • 也许使用类似 Effort 的东西 mock EF - 我从未使用过它,不确定是否还有其他人在野外使用它?
  • 不用测试任何简单地回调到 EF 的东西——所以本质上直接调用 EF 的服务方法(getAll 等)没有经过单元测试,而只是经过集成测试?

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

    最佳答案

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

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

    然而严格来说,我们正在走出单元测试领域并进入集成测试领域,但原则保持不变。

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

    有几件事情需要考虑:

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

  • 设置数据库有两种主要方法,第一种是运行 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);
      }
    }
    

    这里要注意的关键是两个循环的 session 是完全独立的。在您的 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);
    }
    

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

  • 我觉得这使测试类更简单,测试更精细( 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);
      }
    }
    

    但是,确保仅加载正确实体的引用可能会使用 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());
          });
       }
    }
    

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

    关于c# - 人们如何使用 Entity Framework 6 进行单元测试,你应该打扰吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22690877/

    相关文章:

    c# - Entity Framework 插入错误的数据?

    Python:TypeError:<lambda>() 采用 0 个位置参数,但由于断言给出了 1 个

    c++ - 如何在 cppunit 中断言语句抛出 Excp1 或 Excp2 类型的异常?

    javascript - 打印 HTML 页面时会添加额外的零

    C# HttpWebRequest SEC_I_RENEGOTIATE 间歇性错误

    c# - 在 MVC5 中使用 OWIN 在 IdentityServer 中使用被动身份验证

    java - 验证是否从构造函数调用了方法

    c# - NullReferenceException - 无法弄清楚为什么初始化对象重置为 null

    asp.net-mvc - 关于模型验证,EF4 代码优先与模型优先

    c# - 检查 DbSet<T> 是否为空