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

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

有帮助吗?

解决方案

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

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

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

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

其他提示

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

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

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

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

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

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

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

例如:

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

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

将调用抽象出来,然后插入一个仅返回预期数据的模拟。如果您的类除了执行查询之外没有做更多的事情,那么甚至可能不值得测试它们......

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

希望这可以帮助。

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

您有以下选择:

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

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

    class MockDatabase 扩展数据库 { public 结果查询(字符串查询) { 返回 “mock result”;} }

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

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

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

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

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

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

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

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

希望这对您有所帮助,如果没有其他的话,您现在可以搜索一些术语。

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

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

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

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

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

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

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

抱歉,我没有任何 PHP/Python 的具体代码示例,但如果您想查看 .NET 示例,我有一个 邮政 这描述了我用来进行相同测试的技术。

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top