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

Tei*_*ion 146 database unit-testing

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

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

Dou*_*g R 78

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

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在在单元测试中,您创建了一个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)
    }
}
Run Code Online (Sandbox Code Playgroud)

简而言之,这是一种常见的模拟方案.当然,你仍然可能想要对你的实际数据库调用进行单元测试,为此你需要访问数据库.

  • 模拟单元测试中的数据库调用有什么价值?它似乎没有用,因为您可以更改实现以返回不同的结果,但是您的单元测试将(错误地)通过。 (2认同)
  • @bmay2 你没有错。我最初的答案是很久以前(9 年!)写的,当时很多人没有以可测试的方式编写代码,并且严重缺乏测试工具。我不会再推荐这种方法了。今天,我将只建立一个测试数据库并用我需要的测试数据填充它,和/或设计我的代码,这样我就可以在没有数据库的情况下测试尽可能多的逻辑。 (2认同)

Sea*_*ers 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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

  • 而这位亲爱的朋友,被称为整合测试 (8认同)
  • 该问题的作者表示,这是一个适用于与DB有关的所有语言的一般性问题. (2认同)

Ala*_*lan 10

当我们开始研究包含大量"业务逻辑"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下得到类似的东西.

希望这可以帮助.


Mar*_*nke 9

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

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


Chr*_*mer 6

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


akm*_*mad 5

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

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


Mar*_*cin 5

您有以下选择:

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

    类数据库{
     公共结果查询(字符串查询){...真实数据库在这里...}
    }

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

    类 ObjectThatUsesDB { 公共 ObjectThatUsesDB(数据库 db){ 这个.数据库 = db; } }

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

  • 在大多数代码中根本不要使用数据库(无论如何,这是一个不好的做法)。创建一个“数据库”对象,该对象不会返回结果,而是返回普通对象(即返回而User不是元组{name: "marcin", password: "blah"})使用临时构造的真实对象编写所有测试,并编写一个依赖于确保此转换的数据库的大型测试工作正常。

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