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

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

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

95635 次浏览

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

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

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

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

如果使用 PHP,则创建一组特定的类 仅此而已用于数据访问。确保您的对象不知道它们是如何持久化的,并在应用程序类中连接这两个对象。

另一种选择是使用嘲讽/存根。

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

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

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

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

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

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

我从来没有在 PHP 中这样做过,也从来没有使用过 Python,但是您想要做的是模拟对数据库的调用。要做到这一点,您可以实现一些 物流中心无论是第三方工具或您自己管理它,然后您可以实现一些模拟版本的数据库调用程序,这是您将控制该虚假调用的结果。

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

希望这对你有所帮助,如果你现在没有其他需要搜索的词汇的话。

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

XUnit 测试模式这本书描述了一些处理单元测试代码的方法。我同意其他人的说法,你不想这样做,因为它是缓慢的,但你必须做到这一点的时候,我的天。模拟数据库连接来测试更高级别的东西是一个好主意,但是请参阅本书,了解有关与实际数据库交互的建议。

如果您的项目具有很高的内聚力和松散耦合,那么对数据库访问进行单元测试是非常容易的。通过这种方式,您可以只测试每个特定类所做的事情,而不必一次测试所有事情。

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

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

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

对不起,我没有任何 PHP/Python 的特定代码示例,但是如果您希望看到。NET 的例子,我有一个 邮寄,它描述了一个技术,我用来做这个非常相同的测试。

你可以选择:

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

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

    类 MockDatabase 扩展数据库{ 公共结果查询(字符串查询){ 输入「模拟结果」 ; } }

    类 ObjectThatUseDB { Public ObjectThatUseDB (数据库 db){ Database = db; } }

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

  • 绝对不要在大部分代码中使用 DB (这是一种不好的做法)。创建一个“数据库”对象,它不返回结果,而是返回普通对象(即返回 User而不是元组 {name: "marcin", password: "blah"}) ,用特别构造的 真的对象编写所有测试,并编写一个大测试,该测试依赖于一个确保这种转换工作正常的数据库。

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

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

例如:

    [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,另一个在构建后激活,处理单元测试、代码分析、打包等。.

我建议你去数据库查查电话记录。Mocks 基本上是一些对象,它们看起来像您试图调用方法的对象,因为它们具有相同的属性、方法等可供调用方使用。但是,当调用某个特定方法时,它不执行程序设定的任何操作,而是完全跳过该操作,只返回一个结果。这个结果通常是由您提前定义的。

为了设置你的对象进行嘲讽,你可能需要使用一些控制反转/依赖注入模式,如下面的伪代码:

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

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

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

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

一旦这样做了,我们就可以在我们的代码中做类似的事情(我们使用 C + + ,但是我相信你已经明白了) :

GetDatabase () . ExecuteSQL (“ INSERT INTO foo (bla)”)

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

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

我们仍然处于这个过程的早期阶段,但是到目前为止看起来还不错,但是我们必须确保创建任何需要的表并用测试数据填充它们——然而我们已经通过创建一组通用的 helper 函数来减少一些工作负载,这些函数可以为我们做很多这方面的工作。

总的来说,它对我们的 TDD 过程有很大的帮助,因为为了修复某些 bug 而做一些看似无伤大雅的更改,可能会对系统的其他领域(难以检测)产生很奇怪的影响——这是由于 sql/数据库的本质。

显然,我们的经验主要集中在 C + + 开发环境上,但是我确信您可以在 PHP/Python 下获得类似的工作。

希望这个能帮上忙。

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

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