小编典典

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

all

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

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

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

我想这是一个问题,这样做有什么意义吗?如果是这样,鉴于 IQueryable 引起的泄漏抽象以及Ladislav
Mrnk关于单元测试主题的许多精彩帖子并不简单,因为在使用内存时,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 等)不是单元测试的,而只是集成测试的?

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


阅读 58

收藏
2022-08-16

共1个答案

小编典典

这是我非常感兴趣的一个话题。有很多纯粹主义者说你不应该测试 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);
  }
}

这里要注意的关键是两个循环的会话是完全独立的。在您的 RunTest 实现中,您必须确保上下文已提交和销毁,并且您的数据只能来自第二部分的数据库。

2014 年 13 月 10 日编辑

我确实说过我可能会在接下来的几个月里修改这个模型。虽然我在很大程度上支持我上面提倡的方法,但我稍微更新了我的测试机制。我现在倾向于在 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 逻辑已从测试方法本身中删除

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

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

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

2022-08-16