测试,1,2,3...测试...检查?

发布日期 : 12/20/2004 | 更新日期 : 12/20/2004

S. A. Miller

所有优秀的程序员都会测试他们的代码。但测试可以变得更容易一些吗?它可以变得更好一些吗?S. A. Miller 说明了单元测试能够提供哪些帮助,并且说明了一个称为 TSQLUnit 的开放源码测试框架。

如果您像我一样,则可能已经花费了很多时间在查询分析器中开发代码。在您对代码感到满意之后,可以立即对开发服务器上的测试数据库运行一个或两个专设 测试。如果看起来没有什么问题,您便可以将代码投入生产。如果这是一段关键代码,或者该代码较为复杂,则您可能会执行更多的检查,以避免后验剖析。甚至在这种情况下,您也可能屏息以待。

这就是我在大部分职业生涯中所采用的编码方式。哦,有时我会存储测试查询以供将来使用,这通常是因为总裁/CEO/CIO/部门经理习惯于大约每周就改变一下他或她的要求。但是,除此以外,我不会再做什么。我通常在查询分析器或它的 Oracle/Access/FoxPro 等效工具外部用专设 查询进行测试。更高强度的测试需要使用查询分析器调试器。在绝望的情形下,需要使用 PRINT 语句。

目前存在 一种更好的方式。

超越专设 测试

当我的 SIL 部门采用极限编程 (XP) 时,我们还采用了该方法论的单元测试部分,而它们两者都使我成为更出色的开发人员。但是,即使您不在 XP 环境中工作,您仍然可以从 XP 风格的单元测试中获益。

单元测试不同于接受测试。单元测试用于测试较小的代码块(例如,存储过程),而接受测试更多地涉及到用户是否可以接受 UI。以下是我发现的单元测试的五个优点:

  • 它们能够找出应该承担责任的当事人。您是否收到过电子邮件,告诉您应该修复程序错误,而这实际上是其他某个人所作更改的副作用?好,如果您具有一些零散的测试查询,请将它们包装到可以定期运行(或许是在晚上)的存储过程中。请确保在单元测试失败时能够生成电子邮件。

  • 生成库不需要花费很长时间。每个存储过程和每个存储函数都应当具有为它编写的测试,而触发器也应该如此。如果这听起来有些苛刻,那么请想一想,能够在问题到达生产服务器之前捕捉到它,从而拯救您自己,将会是一种多么好的感觉。如果您具有大量旧式代码,那么为每个单元编写测试可能需要多年的工作,并且您也不能仅仅为了编写测试而停止新的开发工作。但是,您可以为每段新代码编写测试,也可以为您修改的每个过程编写测试。用不了多长时间,您就会为关键的旧式代码和新代码编写众多的测试。

  • 轻松创建准确的代码文档。每个过程或函数都应当用不同的参数组合调用。这不仅能够确保代码按预期方式工作,而且还提供了有关您的工作的最新而准确的文档。另外一个编码员只需查看您的测试,就可以了解对您的过程进行调用的示例。谁知道呢?某一天,这另一个编码员可能就是您自己。

  • 它们迫使您预先进行一点儿思考和计划。您应当在编写实际的过程或函数之前编写自己的单元测试。“什么?”您说,“我抗议!我们如何为尚未进行编码的东西编写测试?”

  • 有一个很老的笑话,它讲的是:有一个经理说:“我将弄清楚他们需要什么。其余的人开始编码。”那么,编码员在知道他们需要编写什么之前是无法开始工作的,不是吗?当您首先编写测试时,您将被迫考虑在开始编写该过程之前,您希望该过程完成什么工作。

  • 它们确实可以节省您的时间。开发人员经常抱怨,编写测试需要花费比编写实际过程更多的时间。有时的确如此。但是请考虑以下情况:我最近接受了一项任务,即,修改我曾经遇到过的最难的存储过程之一。它是旧式代码,但是我仍然首先编写了测试。它花费了我几天的时间才完成,部分原因在于对该过程所施加的要求。实践证明,出于我刚才列出的所有原因,该测试非常重要,并且当我必须重新编写该过程以改善性能时,它变得弥足珍贵。

  • 单元测试显示重新编写的过程中存在大量错误,而我能够很快地找到每个错误的根源,所花费的时间只占不使用单元测试时的几分之一。然后,当我认为已经完成该任务时,模糊测试失败了。主循环中的变量之一存在缺陷。如果代码以这种状态发布到生产环境中,那么这将是一个难以捕获的程序错误。最终,我以比采用其他方式更快的速度完成了这项任务。

如何编写 T-SQL 单元测试

在我告诉您有关 T-SQL 测试框架的内容之前,首先需要提醒您注意两个非常基本的原则:

  • 第一,您需要一个具有良好测试数据的数据库。我用“良好数据”表示来自现实世界的真实数据。无论您是一个多么优秀的程序员,都无法充分地为应用程序仿造数据。即使要替换的旧式系统由纸张组成,也要找一位数据录入员来在某些表中输入数据。完成获得真实数据所需的工作。[尽管如此,仍然存在测试数据生成器。请参见本期中我的提示“生成测试数据”— 编者]

  • 第二,不应当针对生产数据库进行开发。您应当具有一个开发或测试数据库,以便满足您自己的需要。过去,当我在 Oracle 进行开发时,我曾经花费了一周的时间将开发数据库放在一个陈旧的服务器上。SQL Server 开发人员没有这样的借口。

在为开发数据库配备良好的数据以后,您需要某种框架以便运行测试。您可以编写自己的框架,但是为什么要这么做呢?已经有一个可用的框架了。

TSQLUnit 简介

TSQLUnit 是 T-SQL 的一个开放源码单元测试框架,它由 Henrik Ekelund 编写,并且可以从 http://sourceforge.net/projects/tsqlunit 获得。以下是一个有关我如何使用它的示例。

我的 TSQLUnit 测试采用了类似的三部分模式:1) 单元测试设置,2) 执行目标过程,和 3) 检查结果。

在单元测试设置过程中,我经常进行检查,以确保没有人趁我不注意时破坏了我的数据:

DECLARE @nId INT, @nNewId INT —- @nNewId is for later
SELECT @nId = [ID] FROM MyTable 
WHERE MyField = 'whatever'
IF @nId IS NULL  -- or @@ROWCOUNT = 0
  EXEC tsu_failure 'The data has changed. 
  ''whatever'' couldn''t be found'

IF 块用于检查预期的记录。如果找不到该记录,则测试会失败,并且会生成错误信息。测试框架移动至下一个单元测试。您不需要在失败消息字符串中使用该单元测试的名称,因为当测试失败时,TSQLUnit 将为您命名它。

现在,我调用将要编写的存储过程:

EXEC CreateMyTableNewRec @nId, @nNewId OUTPUT

正如您看到的那样,我已经确定了需要来自这一新过程的输出参数。在检查结果的过程中,我确保输出参数确实填充了某些内容:

IF @nNewId IS NULL
  EXEC tsu_failure 
  'A new record was not created for table MyTable.'

我可以进一步检查该值,以查看新记录是否是按照我希望的方式创建的。

每个 TSQLUnit 测试本身都是一个存储过程。清单 1 显示了在将上述所有代码段放在一起时所具有的样子:

清单 1. T-SQL 的完整单元测试。

CREATE PROCEDURE ut_MyTable_NewRec
AS
  --== Setup ==--
  DECLARE @nID INT, @nNewId INT
  SELECT @nId = ID FROM MyTable 
  WHERE MyField = 'whatever'
  IF @nId IS NULL  -- or @@ROWCOUNT = 0
    EXEC tsu_failure 'The data has changed. 
    ''Whatever'' couldn''t be found'
  --== Execute ==--
  EXEC CreateMyTableNewRec @nId, @nNewId OUTPUT
  --== Check ==--
  IF @nNewId IS NULL
    EXEC tsu_failure 'A new record was not created 
    for table MyTable.'
GO

请注意存储过程的三部分名称 — ut_MyTable_NewRec。前缀“ut_”提醒 TSQLUnit 这是一个它应当运行的单元测试。如果您已经将前缀 ut_ 用于其他目的,则 TSQLUnit 允许您将它设置为其他内容。“MyTable”是相关单元测试组(称为测试)的名称。例如,您可以添加另一个名为 ut_MyTable_DeleteRec 的单元测试。MyTable 组将同时对在 MyTable 中添加和删除记录进行测试。该组可以独立于其他测试组运行。该名称的第三部分 —“NewRec”或“DeleteRec”唯一地标识该单元测试。

请注意,您不需要在每个单元测试中使用 BEGIN TRAN 和 ROLLBACK;TSQLUnit 会为您处理该问题。

运行单元测试

为了运行清单 1 中的单元测试,您需要设置框架。从查询分析器中,对开发数据库运行 tsqlunit.sql。您只需要对该数据库执行一次该操作。接下来,创建过程 ut_MyTable_NewRec(如果您尚未创建的话)。现在,您已经准备就绪了。只须执行以下单元测试:

-- This will run all tests for suite MyTable, 
EXEC tsu_RunTests MyTable

固定件

假设我希望使许多记录可供一个组中的所有 单元测试使用。我不希望为每个测试编写相同的设置代码。TSQLUnit 通过设置固定件 解决了这个问题。固定件中的代码将在每个单元测试之前运行。

例如,上述 MyTable 组的设置固定件将命名为 ut_MyTable_setup。该名称的第三部分“setup”提醒 TSQLUnit 将该过程视为该组的设置固定件。它将如下所示:

CREATE PROCEDURE ut_MyTable_setup
AS
  INSERT INTO MyTable ([Description]) 
  VALUES ('something')
  --( more records inserted here
GO

SQL Server 社区非常感谢 Henrik Ekelund 和他的雇主使 TSQLUnit 成为开放源码。

链接到

http://sourceforge.net/projects/tsqlunit

链接到

http://tsqlunit.sourceforge.net/tsqlunit_cookbook.htm (文档)

有关 SQL Server Professional 和 Pinnacle Publishing 的详细信息,请访问它们位于 http://www.pinpub.com/ 的 Web 站点。

注:这不是 Microsoft Corporation 的 Web 站点。Microsoft 对该 Web 站点的内容不承担任何责任。

本文是从 SQL Server Professional 2004 年 9 月刊转载的。版权所有 2004,Pinnacle Publishing, Inc.(除非另行说明)。保留所有权利。SQL Server Professional 是 Pinnacle Publishing, Inc. 独立发行的产品。未经 Pinnacle Publishing, Inc. 事先同意,不得以任何形式使用或复制本文的任何部分(评论文章中的简短引用除外)。要联系 Pinnacle Publishing, Inc.,请致电 1-800-788-1900。

转到原英文页面

显示: