如何使用数据库查询对对象进行单元测试

时间:2008-08-27 17:46:03

标签: database unit-testing

我听说单元测试“非常棒”,“非常酷”和“各种好东西”,但70%或更多的文件涉及数据库访问(有些读取和写入)我就是不知道如何为这些文件编写单元测试。

我正在使用PHP和Python,但我认为这是一个适用于大多数/所有使用数据库访问的语言的问题。

13 个答案:

答案 0 :(得分:75)

我建议嘲笑你对数据库的调用。模拟基本上是看起来像你试图调用方法的对象的对象,因为它们具有调用者可用的相同属性,方法等。但是,当调用特定方法时,它不会执行他们编程要执行的任何操作,而是完全跳过它,并返回结果。该结果通常由您提前定义。

为了设置对象进行模拟,您可能需要使用某种控制/依赖注入模式的反转,如下面的伪代码所示:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

现在在您的单元测试中,您创建了一个FooDataProvider的模拟,它允许您调用方法GetAllFoos而无需实际命中数据库。

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

一个常见的模拟场景,简而言之。当然,您仍然可能希望对您的实际数据库调用进行单元测试,您需要访问该数据库。

答案 1 :(得分:25)

理想情况下,你的对象应该是持久无知的。例如,您应该有一个“数据访问层”,您将向其发出请求,这将返回对象。这样,您可以将该部分从单元测试中删除,或者单独测试它们。

如果您的对象与数据层紧密耦合,则很难进行适当的单元测试。单元测试的第一部分是“单元”。所有装置都应该能够单独进行测试。

在我的c#项目中,我使用NHibernate与完全独立的数据层。我的对象存在于核心域模型中,可以从我的应用层访问。应用程序层与数据层和域模型层进行通信。

应用程序层有时也称为“业务层”。

如果您使用的是PHP,请为数据访问创建一组特定的类。确保您的对象不知道它们是如何持久化的,并将两者连接在应用程序类中。

另一种选择是使用模拟/存根。

答案 2 :(得分:11)

使用数据库访问对对象进行单元测试的最简单方法是使用事务范围。

例如:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

这将恢复数据库的状态,基本上就像事务回滚一样,因此您可以根据需要多次运行测试,而不会产生任何副作用。我们在大型项目中成功使用了这种方法。我们的构建需要一段时间才能运行(15分钟),但是进行1800次单元测试并不可怕。此外,如果构建时间是一个问题,您可以将构建过程更改为具有多个构建,一个用于构建src,另一个构建后处理单元测试,代码分析,打包等...

答案 3 :(得分:9)

如果要对类进行单元测试,则应该模拟数据库访问。毕竟,您不希望在单元测试中测试数据库。那将是一次整合测试。

抽象调用,然后插入一个只返回预期数据的模拟。如果你的类不只是执行查询,它甚至可能不值得测试它们,但是......

答案 4 :(得分:9)

当我们开始研究包含大量“业务逻辑”sql操作的中间层流程的单元测试时,我或许可以让您体验一下我们的体验。

我们首先创建了一个抽象层,允许我们“插入”任何合理的数据库连接(在我们的例子中,我们只支持单个ODBC类型的连接)。

一旦这个到位,我们就可以在我们的代码中做这样的事情(我们在C ++中工作,但我相信你明白了):

GetDatabase()。ExecuteSQL(“INSERT INTO foo(blah,blah)”)

在正常运行时,GetDatabase()将返回一个对象,该对象通过ODBC直接向数据库提供所有sql(包括查询)。

然后我们开始研究内存数据库 - 最好的方法似乎是SQLite。 (http://www.sqlite.org/index.html)。它的设置和使用非常简单,并允许我们使用子类和覆盖GetDatabase()将sql转发到为每次执行的测试创建和销毁的内存数据库。

我们还处于早期阶段,但到目前为止看起来还不错,但是我们必须确保创建所需的任何表并用测试数据填充它们 - 但是我们已经在某种程度上减少了工作量这里通过创建一组通用的辅助函数,可以为我们完成所有这些。

总的来说,它对我们的TDD过程有很大的帮助,因为修复某些错误看起来非常无害的修改可能会对系统的其他(难以检测)区域造成相当奇怪的影响 - 由于sql的本质/数据库。

显然,我们的经验主要围绕C ++开发环境,但我相信你可能会在PHP / Python下得到类似的东西。

希望这有帮助。

答案 5 :(得分:5)

本书xUnit Test Patterns描述了处理命中数据库的单元测试代码的一些方法。我同意那些说你不想这样做的人,因为它很慢,但你必须在某个时候这样做,IMO。模拟数据库连接以测试更高级别的东西是一个好主意,但请查阅本书,了解有关与实际数据库交互可以执行的操作的建议。

答案 6 :(得分:4)

您拥有的选项:

  • 编写一个脚本,在开始单元测试之前擦除数据库,然后使用预定义的数据集填充db并运行测试。你也可以在每次测试之前做到这一点 - 它会很慢,但不容易出错。
  • 注入数据库。 (伪Java中的示例,但适用于所有OO语言)

    class Database {
     public Result query(String query) {... real db here ...}
    }

    class MockDatabase extends Database { public Result query(String query) { return "mock result"; } }

    class ObjectThatUsesDB { public ObjectThatUsesDB(Database db) { this.database = db; } }

    现在在生产中你使用普通数据库,对于所有测试你只需要注入你可以临时创建的模拟数据库。

  • 在大多数代码中根本不使用DB(无论如何这都是一种不好的做法)。创建一个“数据库”对象而不是返回结果将返回正常对象(即将返回User而不是元组{name: "marcin", password: "blah"})使用ad hoc构造的 real 对象并编写一个依赖于数据库的大测试,以确保此转换正常。

当然这些方法并不相互排斥,您可以根据需要混合搭配。

答案 7 :(得分:2)

我经常尝试在测试对象(和ORM,如果有的话)和测试数据库之间分解我的测试。我通过模拟数据访问调用来测试事物的对象方面,而我通过测试与db的对象交互来测试数据库方面,根据我的经验,这通常相当有限。

我曾经对编写单元测试感到沮丧,直到我开始模拟数据访问部分,所以我不必创建测试数据库或动态生成测试数据。通过模拟数据,您可以在运行时生成所有数据,并确保您的对象可以使用已知输入正常工作。

答案 8 :(得分:2)

您可以使用模拟框架来抽象出数据库引擎。我不知道PHP / Python是否有一些但是对于类型化的语言(C#,Java等)有很多选择

这还取决于您如何设计这些数据库访问代码,因为某些设计比其他帖子提到的更容易进行单元测试。

答案 9 :(得分:2)

我从来没有在PHP中这样做,我从来没有使用过Python,但你想要做的就是模拟对数据库的调用。要做到这一点你可以实现一些IoC无论是第三方工具还是你自己管理它,然后你可以实现数据库调用者的一些模拟版本,你可以在那里控制那个假电话的结果。

只需通过编码到接口即可执行简单形式的IoC。这需要在你的代码中进行某种面向对象,所以它可能不适用于你所做的事情(我说因为我只需要你提到PHP和Python)

希望这很有帮助,如果没有别的,你现在就可以搜索一些术语了。

答案 10 :(得分:2)

我同意第一篇文章 - 数据库访问应该被剥离到实现接口的DAO层。然后,您可以针对DAO层的存根实现测试您的逻辑。

答案 11 :(得分:2)

如果您的项目具有高内聚和松散耦合,则单元测试数据库访问非常容易。这样,您可以只测试每个特定类所做的事情,而无需一次测试所有内容。

例如,如果您对用户界面类进行单元测试,那么您编写的测试应该只尝试验证UI内部的逻辑是否按预期工作,而不是该函数背后的业务逻辑或数据库操作。

如果你想对实际的数据库访问进行单元测试,你实际上会得到更多的集成测试,因为你将依赖于网络堆栈和数据库服务器,但是你可以验证你的SQL代码是做什么的。要求它做。

我个人测试的隐藏能力一直是它迫使我以比没有它们更好的方式设计我的应用程序。这是因为它真的帮助我摆脱了“这个功能应该做的一切”的心态。

抱歉,我没有PHP / Python的任何特定代码示例,但如果您想查看.NET示例,我有一个post,它描述了我用来执行相同测试的技术。< / p>

答案 12 :(得分:2)

为单元测试设置测试数据可能是一项挑战。

对于Java,如果使用Spring API进行单元测试,则可以在单元级别上控制事务。换句话说,您可以执行涉及数据库更新/插入/删除的单元测试并回滚更改。在执行结束时,您将所有内容保留在数据库中,就像开始执行之前一样。对我来说,它是最好的。