【问题标题】:Mocking a Linq2Sql DataContext模拟 Linq2Sql 数据上下文
【发布时间】:2012-07-30 07:10:02
【问题描述】:

我有一个 Lin2Sql DataContext,我用它来从 sql 数据库中获取我的所有数据,但是我正在努力寻找一种方法来成功地模拟它,以便我可以创建相关的单元测试。

在我想要测试的数据访问对象中,我每次都在刷新上下文,我发现很难找到一种简单合适的方法来模拟它。

对于此事的任何帮助将不胜感激。

【问题讨论】:

    标签: c# unit-testing c#-4.0 linq-to-sql mocking


    【解决方案1】:

    模拟 linq-to-sql 上下文确实是一项艰巨的任务。我通常通过让我的单元测试针对单独的数据库副本运行它来解决这个问题,其中的数据经过专门设计以适合单元测试。 (我知道可以说它不再是单元测试,而是集成测试,但我不在乎,只要我得到测试的代码)。

    为了使数据库保持在已知状态,我将每个测试包装在一个TransactionScope 中,该TransactionScope 在测试结束时回滚。这样数据库的状态就永远不会改变。

    示例测试方法如下所示:

    [TestMethod]
    public void TestRetire()
    {
        using (TransactionScope transaction = new TransactionScope())
        {
            Assert.IsTrue(Car.Retire("VLV100"));
            Assert.IsFalse(Car.Retire("VLV100"));
    
            // Deliberately not commiting transaction.
        }
    }
    

    代码来自一篇关于我前段时间写的方法的博文:http://coding.abel.nu/2011/12/using-transactions-for-unit-tests/

    【讨论】:

    • +100!:这是测试 DAL 的方式。模拟 ORM/数据访问提供者是困难的、耗时的,并且通常会导致非常脆弱的测试。不要单元测试 DAL(纯单元测试风格);按照安德斯的建议去做。
    • 这非常有效,让我可以在单独的数据库上创建有效的测试并测试我所有的 linq 查询和映射到 POCO 的正确性,谢谢
    • 这是我想采用的一般方法。但是,测试数据库是如何维护和版本化的?如果可能的话,我不希望有单独的独立流程或大型数据库 - 一些使用 MDF 的明智方式? SQL Express?
    • 我使用过数据生成计划(在 VS2008 中,不了解 VS2012)、简单的脚本来填充一些最小的测试数据,甚至使用了一个由单元填充数据的空 DB测试初始化​​器。无论如何,单元/集成测试数据库应该非常小,每个表中只有几条记录。
    【解决方案2】:

    简而言之,您不要模拟 DataContext。您从中提取接口并使用实体集的一些集合来模拟该接口,然后验证这些集合的内容。

    【讨论】:

      【解决方案3】:

      由于您请求一种模拟DataContext 的方法,我假设您确实想做一些单元测试而不是集成测试

      好吧,我会告诉你如何做到这一点,但首先我想鼓励你阅读以下链接,它们都是关于编写干净的可测试代码

      并检查此响应中的链接:

      观看 Misko Hevery 的简洁代码演讲(提供给 Google 人员)

      我曾经对自己和同事重复的一件事是,任何人都可以编写单元测试,因为它们很容易编写。所以一个简单的测试本质上就是进行一些比较,如果结果失败则抛出异常,任何人都可以这样做。当然,有数百个框架可以帮助我们以优雅的方式编写这些测试。但是真正的交易和真正的努力应该放在学习如何编写干净的可测试代码

      即使您聘请 Misko Hevery 来帮助您编写测试,如果您的代码对测试不友好,他也会很难编写测试。

      现在模拟 DataContext 对象的方法是:不要这样做

      改为使用自定义接口包装调用:

      public interface IMyDataContextCalls
      {
          void Save();
          IEnumerable<Product> GetOrders();
      }
      // this will be your DataContext wrapper
      // this wll act as your domain repository
      public class MyDataContextCalls : IMyDataContextCalls
      {
          public MyDataContextCalls(DataClasses1DataContext context)
          {
              this.Context = context;
          }
      
          public void Save()
          {
              this.Context.SubmitChanges();
          }
      
          public IEnumerable<Product> GetOrders()
          {
              // place here your query logic
              return this.Context.Products.AsEnumerable();
          }
      
      
          private DataClasses1DataContext Context { get; set; }
      
      }
      
      // this will be your domain object
      // this object will call your repository wrapping the DataContext
      public class MyCommand
      {
          private IMyDataContextCalls myDataContext;
          public MyCommand(IMyDataContextCalls myDataContext)
          {
              this.myDataContext = myDataContext;
          }
      
          public bool myDomainRule = true;
      
          // assume this will be the SUT (Subject Under Test)
          public void Save()
          {
              // some business logic
              // this logic will be tested
              if (this.myDomainRule == true)
              {
                  this.myDataContext.Save();
              }
              else
              {
                  // handle your domain validation  errors
                  throw new InvalidOperationException();
              }
          }
      }
      
      [TestClass]
      public class MyTestClass
      {
          [TestMethod]
          public void MyTestMethod()
          {
              // in this test your mission is to test the logic inside the 
              // MyCommand.Save method
              // create the mock, you could use a framework to auto mock it
              // or create one manually
              // manual example:
              var m = new MyCommand(new MyFakeDataContextFake());
      
              m.Invoking(x => x.Save())
                  //add here more asserts, maybe asserting that the internal
                  // state of your domain object was changed
                  // your focus is to test the logic of the domain object
                  .ShouldNotThrow();
      
              //auto mock example:
              var fix = new Fixture().Customize(new AutoMoqCustomization());
              var sut = fix.CreateAnonymous<MyCommand>();
              sut.myDomainRule = false;
      
              sut.Invoking(x => x.Save())
                  .ShouldThrow<InvalidOperationException>();
          }
      
          public class MyFakeDataContextFake : IMyDataContextCalls
          {
              public void Save()
              {
                  // do nothing, since you do not care in the logic of this method,
                  // remember your goal is to test the domain object logic
              }
      
              public IEnumerable<Product> GetOrders()
              {
                  // we do not care on this right now because we are testing only the save method
      
                  throw new NotImplementedException();
              }
          }
      }
      

      注意事项:

        1234563与任何不想要的依赖解耦。
      • 在特定的MyDataContextCalls 实现中,您明确使用DataClasses1DataContext 上下文,但您可以随时更改实现,并且不会影响您的外部代码,那是因为您总是使用IMyDataContextCalls 接口。因此,您可以随时使用精彩的 NHibernate =) 或可怜的 ef 或模拟的实现来更改例如此实现

      • 最后,但并非最不重要。请仔细检查我的代码,您会注意到域对象中没有new 运算符。这是编写测试友好代码时的愚蠢规则:将在域对象之外创建对象的责任解耦


      我个人在每个项目和我编写的每个测试中都使用三个框架,我非常推荐它们:

      例如,在上面的代码中,我向您展示了如何为您的存储库编写手动 fake,但这显然是我们不想在实际项目中做的事情,想象一下数字您必须编写代码才能编写测试的对象。

      使用 AutoFixture 与 Moq 结合的强大功能:

      这一行:var m = new MyCommand(new MyFakeDataContextFake());

      会变成:

              var fixture = new Fixture().Customize(new AutoMoqCustomization());
              var sut = fixture.CreateAnonymous<MyCommand>();
      

      就是这样,这段代码会自动为MyCommand的构造函数中需要的所有对象创建模拟。

      【讨论】:

      • 感谢您的帮助,我现在会查看您提供的链接。
      • 不幸的是,它不允许模拟已编译的查询,因为它们需要将 DataContext 的实例作为参数传递。
      猜你喜欢
      • 2012-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-18
      • 1970-01-01
      • 1970-01-01
      • 2022-07-28
      • 2017-04-01
      相关资源
      最近更新 更多