【问题标题】:How to unit test an object with database queries如何使用数据库查询对对象进行单元测试
【发布时间】:2010-09-07 00:57:09
【问题描述】:

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

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

【问题讨论】:

    标签: database unit-testing


    【解决方案1】:

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

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

    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)
        }
    }
    

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

    【讨论】:

    • 我知道这很旧,但是如何为已经在数据库中的表创建一个重复表。这样您就可以确认数据库调用是否有效?
    • 我一直使用 PHP 的 PDO 作为我对数据库的最低级别的访问,我从中提取了一个接口。然后我在此之上构建了一个应用程序感知数据库层。这是包含所有原始 SQL 查询和其他信息的层。应用程序的其余部分与这个更高级别的数据库交互。我发现这对于单元测试非常有效;我测试我的应用程序页面如何与应用程序数据库交互。我测试我的应用程序数据库如何与 PDO 交互。我假设 PDO 工作时没有错误。源代码:manx.codeplex.com
    • @bretterer - 创建一个重复的表有利于集成测试。对于单元测试,您通常会使用一个模拟对象,它允许您测试一个代码单元,而不管数据库如何。
    • 在单元测试中模拟数据库调用有什么价值?它似乎没有用,因为您可以更改实现以返回不同的结果,但是您的单元测试会(错误地)通过。
    • @bmay2 你没看错。我最初的答案是很久以前(9 年!)写的,当时很多人没有以可测试的方式编写代码,而且测试工具严重缺乏。我不会再推荐这种方法了。今天我只是建立一个测试数据库,并用我测试所需的数据填充它,和/或设计我的代码,这样我就可以在没有数据库的情况下测试尽可能多的逻辑。
    【解决方案2】:

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

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

    在我的 c# 项目中,我使用 NHibernate 和一个完全独立的数据层。我的对象位于核心域模型中,可以从我的应用程序层访问。应用层与数据层和领域模型层对话。

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

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

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

    【讨论】:

    • 我一直同意这一点,但实际上由于截止日期和“好吧,现在只添加一项功能,今天下午 2 点”这是最难实现的事情之一。但是,如果我的老板决定他没有想到需要全新业务逻辑和表的 50 个新的紧急问题,那么这种事情就是重构的主要目标。
    • 如果您的对象与数据层紧密耦合,则很难进行适当的单元测试。单元测试的第一部分,是“单元”。所有单元都应该能够单独进行测试。很好的解释
    【解决方案3】:

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

    例如:

        [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,另一个用于随后启动以处理单元测试、代码分析、打包等...

    【讨论】:

    • +1 在对数据访问层进行单元测试时节省大量时间。请注意,TS 通常需要 MSDTC,这可能是不可取的(取决于您的应用是否需要 MSDTC)
    • 原来的问题是关于 PHP 的,这个例子似乎是 C#。环境非常不同。
    • 该问题的作者表示,这是一个适用于所有与数据库有关的语言的一般性问题。
    • 还有这个亲爱的朋友,叫做集成测试
    【解决方案4】:

    如果你想对你的类进行单元测试,你应该模拟数据库访问。毕竟,您不想在单元测试中测试数据库。那将是一个集成测试。

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

    【讨论】:

      【解决方案5】:

      当我们开始对包含大量“业务逻辑”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 下获得类似的东西。

      希望这会有所帮助。

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          【解决方案7】:

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

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

          【讨论】:

            【解决方案8】:

            如果您的项目始终具有高内聚性和松散耦合,那么对您的数据库访问进行单元测试就很容易了。这样,您可以只测试每个特定类所做的事情,而不必一次测试所有内容。

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

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

            对我个人而言,单元测试的隐藏力量在于它迫使我以一种比没有它们时更好的方式设计我的应用程序。这是因为它确实帮助我摆脱了“这个功能应该做所有事情”的心态。

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

            【讨论】:

              【解决方案9】:

              您的选择:

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

                类数据库{ public Result query(String query) {... real db here ...} }

                类 MockDatabase 扩展数据库 { 公共结果查询(字符串查询){ 返回“模拟结果”; } }

                类 ObjectThatUsesDB { 公共 ObjectThatUsesDB(数据库 db){ this.database = 数据库; } }

                现在在生产中,您使用普通数据库,对于所有测试,您只需注入可以临时创建的模拟数据库。
              • 在大多数代码中根本不要使用 DB(无论如何,这是一种不好的做法)。创建一个“数据库”对象,而不是返回结果,而是返回普通对象(即返回 User 而不是元组 {name: "marcin", password: "blah"})用特设构造的 real 对象编写所有测试并编写一项依赖于确保此转换正常工作的数据库的大型测试。

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

              【讨论】:

                【解决方案10】:

                我从未在 PHP 中这样做过,也从未使用过 Python,但您想要做的是模拟对数据库的调用。为此,您可以实现一些IoC,无论是第三方工具还是您自己管理它,然后您可以实现一些数据库调用程序的模拟版本,您将在其中控制该假调用的结果。

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

                希望对您有所帮助,如果没有别的,您现在有一些要搜索的字词。

                【讨论】:

                  【解决方案11】:

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

                  【讨论】:

                    【解决方案12】:

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

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

                    【讨论】:

                      【解决方案13】:

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

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

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2011-05-15
                        • 1970-01-01
                        • 1970-01-01
                        • 2010-10-03
                        • 2018-07-06
                        相关资源
                        最近更新 更多