19

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

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

任何有关此事的帮助将不胜感激。

4

3 回答 3

20

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

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

示例测试方法如下所示:

[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/

于 2012-08-01T11:40:54.817 回答
13

由于您要求一种模拟 a 的方法,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();
        }
    }
}

笔记:

  • 当你声明你的IMyDataContextCalls接口时,你实际上是在抽象 a 的使用DataContext,因此这个接口应该只包含 POCO 对象(大多数时候),如果你遵循这种方法,你的接口将与任何不希望的依赖关系解耦。

  • 在具体MyDataContextCalls实现中,您显式使用DataClasses1DataContext上下文,但您可以随时更改实现,这不会影响您的外部代码,这是因为您始终使用IMyDataContextCalls接口。因此,您可以随时更改此实现,例如使用美妙的NHibernate =) 或可怜的 ef 或模拟的另一个实现

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


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

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

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

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

会变成:

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

And that's it, this code will automatically create mocks for all the objects needed in the constructor of MyCommand.

于 2012-08-01T12:29:25.460 回答
4

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

于 2012-08-01T11:43:47.543 回答