Feuerstein 的“构建代码分析工具”系列的第 2 部分
在本系列的 第一篇文章中,我决定构建一个可以对代码进行质量检查的实用工具:非凡是识别 PL/SQL 程序包中具有歧义或者潜在歧义的超载问题。
此外,我还识别数据源(ALL_ARGUMENTS 数据字典视图)和代码(DBMS_DESCRIBE 程序包),以帮助构建实用工具。下一步要做什么呢?
坦率地说,我自然倾向于打开我所喜爱的集成开发环境 (IDE) 并开始动指如飞地编写代码,投入激动人心的创作之中。我希望在工作时思考并得出结论,不断地应对挑战,使工作成果运行起来,然后对其进行微调。
这种方法有积极的方面(您当然不会有过度设计的风险),但也有很多缺点。首先,假如我所使用的构建方法是直通式构造非线性系统 — 常被亲切地称为 HRCLS 或 Hercules(一种非常轻型的方法论),我最终不会仅是微调代码。绝对不会,在我将范围缩小到最终目的、目标和实质内容时,我最终将会以重写整个程序 — 一次、两次或三次。但是,尽管可以激动地看到,经过自己的努力,实用工具成形、进步并改观,但这会浪费很多的时间。
对于 Codecheck,我会忍住最初的诱惑。我不会在项目开始的头 60 秒内就编写代码,而是做一个简单的声明:我保证使我编写的代码适用于一个全面的测试计划。
这与避免 Hercules 综合症有什么必然的关系呢?让我们来考虑这种保证的含义:
我将制订一个测试计划,它非常全面,精心策划了数十个(甚至可能有 100 个之多)测试方案,包括参数的数量、各种数据类型的结合以及缺省值的有无。
我要设计、构建和运行测试,这些测试可确定我的代码是否满足测试计划的要求:换言之,设计、构建和运行涵盖我的全部测试方案的测试。
天哪,这听起来确实很有道理,不是吗?我的意思是,谁会一直做所有的这些事情?况且,我们都知道这样的事实:我们之中很少有人实际上会花时间,或在手边有必备的工具去做全面的测试。实际上我认为,世界上至少百分之九十的软件开发人员和开发团队(包括我自己)远远不会完全按上述保证去做。
对我来说,在开发初始时我表明要执着地进行测试,我发现这一点促使我实现了 Codecheck.
定义范围,做出假设
在开发一项测试策略并构造测试之前,我需要确定项目的使用范围。用户群通常确定(并经常更改)项目的使用范围。对于 Codecheck 以及我制作的很多其他实用工具,您,开发人员,是我的用户,但我仍然需要确定使用范围,把您的需求作为我的指导。我的目标是产生一个代码体,能立即帮助开发人员提高代码质量,但也可以作为一项稳定而且可扩展的功能,开发人员可以将其加入代码中满足自己的需要。它必须能够处理现实世界中足够的复杂性,这样才有用处,但又不可太复杂,以至于将 Codecheck 开发变成一种全职工作。
当我深入查看 ALL_ARGUMENTS 的内容时,我吃惊地发现那些参数列表会变得非常复杂。例如,在 Oracle9i 的环境中,一个参数可以将记录的结合数组(以前称为表的索引)作为其类型,其中记录的一个字段是另一个结合数组,以此类推。非常坦率地说,我实际上不希望必须编写代码来处理这些参数全部的可能情况。
与此相反,Codecheck 将只基于“0级”参数来检查和进行分析:这是指在 ALL_ARGUMENTS 视图中的那些项目,当其出现在程序头时,它们对应于参数列表。实际上,这可能是您为分析超载而所需的全部要素,但是全部的具体资料可能在进行某种其他的代码检查时派上用场。(在撰写此文章时,我已确定了一种需要更多信息的情况:假如您将一个参数定义为表单 %ROWTYPE 的记录,则 ALL_ARGUMENTS 和 DBMS_DESCRIBE 都不会提供其类型的信息。您可能必须比较那些行类型(1 级或更多级)的各个字段的数据类型以识别岐义。天哪。
定义测试策略
开发 Codecheck 的下一步是探究和定义测试策略。我曾提到过,我承诺编写能够满足测试计划的代码,这种承诺影响着开发过程。例如,我在编写如 Codecheck 等实用工具时的第一个冲动是,制作一个程序,接受一个程序包的名称并在屏幕上显示代码检查的结果。为了确定实用工具是否正确工作,我需要查看屏幕上的检查结果并对其进行分析。这听起来如何?熟悉吗?可伸缩吗?通常这种方法甚至对非常简单的程序都无效。
Codecheck 测试计划无疑会有很多测试方案,而其结果并不明确。换言之,除非我记住成功对所有这些测试方案意味着什么,否则我必须依靠手动的、直观的验证(先看这个窗口;然后将其与那个窗口的内容进行比较)。我需要用很长的时间完成一项测试(这是构成责任性的一项相当重要的要素),所以我很少进行测试 — 假如曾做过测试的话。这与我的保证相违反。
我需要找到一种更快捷方便的测试方法。使用 java 的许多开发人员转向 Junit.PL/SQL 开发人员则利用 utPLSQL,一种用于 PL/SQL 开发人员的开放源单元测试框架。(申明:我是 utPLSQL 的创建者,尽管其他人现在也帮助进行实施并制作文档。)
我不会在本文中以很大的篇幅具体描述 utPLSQL 的起源、理论基础或基本工作方式。假如您需要了解比本文内容更具体的情况,请访问 http://utplsql.sourceforge.net/ 或 http://utplsql.oracledeveloper.nl.
utPLSQL 简介
utPLSQL 是一种用于 PL/SQL 程序的测试框架(代码以及使用这种代码的进程的集合)。使用 utPLSQL,您可以构造包含单元测试过程的单元测试程序包,并依照 utPLSQL 的命名规范和测试机制来设计此程序包。然后您只须简单地操作 utPLSQL 来测试程序或程序包。它运行所有测试并自动检测该测试是否成功或失败。它准确指出失败的测试方案,使您更快地确定正确的测试方案并识别应用程序中错误的原因。
这种框架是基于极限编程的单元测试概念 ( www.XPRogramming.com) 以及 Junit(Java 单元测试框架)。下面是一些该方法进行单元测试的基本原则:
在编写代码前编写单元测试。
少做编写和更改,多做测试。
自动执行测试和生成报告:红绿灯式的方法。
图 1 表示 utPLSQL体系结构的一个简化视图,它是以强大有效的方式自动执行测试的一个往返旅程。下面对行程中的各站作一解释:
调用 utPLSQL 测试过程执行测试程序包。utPLSQL 按照动态 PL/SQL 和命名规范来运行任何设置代码,定位并执行单元测试程序,并进行必要的清除(“拆卸”步骤)。
单元测试过程调用“判定 API”,它将测试结果与“控制”条件进行比较。
判定程序将结果(通过或失败)写到下面的结果表中。
tPLSQL 测试读出结果表中的内容,确定该测试的状态。
utPLSQL 根据结果作出报告,或者通过 DBMS_OUTPUT 送到屏幕,或者通过 UTL_FILE 送到一个文件。
图 1:utPLSQL 体系结构的往返旅程简化视图。ppt.
让我们来看一个很简单的示例,从而大致了解单元测试程序包的内容。假设我已经创建了一个关于 SUBSTR 的封装,答应在开始和结束点之间请求一个子串(一个简单的函数)(列表 1)。
即使是简单的或看似不重要的程序(如 betwnStr)也需要测试 — 而实际上我必须考虑大量的测试方案(包括 NULL 开始值、NULL 结束值以及开始大于结束等)。列表 2 显示了测试程序包的一部分(全部测试包的内容请参见 ut_betwnstr.pkb)。关于要害部分的解释,请参见表 1.
也许您会想,“多么单调乏味!我真的必须编写所有那些代码,只是为了测试这个简单的函数吗?”实际上,那些在测试领域工作的人都知道,测试一个应用程序所需要编写的代码数量经常比应用程序本身更多。对于这个特定的程序包和测试代码的 utPLSQL 风格,您在某些情况下会生成全部的测试代码。实际上,ut_betwnstr 程序包通过对 utGen.testpkg_from_string 过程的调用而生成,如列表 3 所示。
即使您不生成测试程序包,也经常可以找到其他方法,利用最少的代码运行许多基于 utPLSQL 的测试 — 这正是我用 Codecheck 所做的工作。
将 utPLSQL 应用于 Codecheck
要利用 utPLSQL,我需要构建一个单元测试程序包并调用 utAssert 程序,以确定我的代码是否通过其测试。但是,在进行这些操作之前,我需要精心制作我的测试方案。请记住:测试方案第一,其次才是代码。
现在应该寻找灵感,暂停前进,考虑一下我所提供的实用工具。我希望它能够验证什么?那些能编译但包含歧义超载的程序包的特例是什么?我能检测到什么情况?有效超载的示例是什么?究竟我需要测试正面和负面的因素。经过一段时间以后,我给出以下的内容:
有效的超载
两个超载程序带有不同数量的非缺省参数。
两个超载程序带有相同数量的非缺省参数,并在数据类型方面具有足够的差别(如 NUMBER 对 DATE)。
函数和过程具有相同名称和参数列表,包括不带参数的情况。区分这两者并没有问题,因为它们在代码中的使用方法不同。
无效的超载
两个超载程序具有不同数量的非缺省参数,导致歧义的参数列表。
两个程序具有单个参数,数据类型相同但参数名不同。
两个程序具有相同的名称,具有单个参数,但却是同系列中不同的数据类型。
一个程序没有参数,第二个程序具有一个带缺省值的参数。
一个程序具有 N 个参数,第二个程序具有 N+1 个参数,全部带有缺省值。
一个程序具有 N 个参数,其中 N-1 个参数有缺省值;第二个程序具有 N+1 个参数,其中最后两个缺省。
一个程序具有一个参数,第二个程序具有两个参数,其第二个参数有缺省值。
我肯定还将考虑其他测试方案,但这些已足够继续进行了。如何最好地进行上述方案的测试?我需要在程序包中定义这些不同的结合。让我们称之为 allargs_test.为每个测试方案使用一个不同的超载程序名称,可能效果会较好。这样可以保持事情清楚易辨而且非常有条理。实际上,我要创建一个表(参见表 2)。
此表有助于工作但还不够。我还需要为这些测试方案分别指定期望的结果。它是有效的超载吗?假如不是,它是如何失败的?我如何用一种能使用 utPLSQL 自动运行测试的方式来获取这种信息?问题太多了。
在本文中,我没有指定所有测试方案的结果全集;我认为大致了解全部过程就足够了。让我们完整地运行表 2 中的一些方案,从而了解我需要测试哪些信息种类。考虑 samefamily1 过程。以下是这一超载的具体说明:
CREATE OR REPLACE PACKAGE allargs_test IS PROCEDURE samefamily1 (arg IN NUMBER);PROCEDURE samefamily1 (arg IN INTEGER);
很清楚,arg 参数的数据类型太相似了,它们都属于数字型。因此,这将是导致故障的歧义超载。对 samefamily1 运行 Codecheck 的结果应该类似于:
allargs_test.samefamily:invalid overloading
结果看来是很基本的东西。我们只须说是或不是吗?或者说无效或有效?在本例中,我想是这样的。但我们再来看另一个更有趣的情况。考虑 noparms2 的开头部分:
CREATE OR REPLACE PACKAGE allargs_test IS PROCEDURE noparms2 (arg1 IN VARCHAR2 := NULL,arg2 IN VARCHAR2 := NULL);
PROCEDURE noparms2 (arg1 IN VARCHAR2 := NULL,arg2 IN VARCHAR2 := NULL,arg3 IN VARCHAR2 := NULL);
关于这两个程序,Codecheck 会告诉我什么呢?让我来计算可能引起 allargs_test.noparms2 无效或歧义的方式:
allargs_test.noparms2;allargs_test.noparms2 ('abc');allargs_test.noparms2 ('abc', 'def');
Codecheck 应该识别 noparms2 的总共三种不同的无效形式。这是由于所有的跟踪缺省值答应我使用不同数量的参数来调用 noparms2.这比第一个测试方案复杂得多。当然,它可以变得更加复杂,这取决于超载程序的数量、参数的数量以及缺省值的有无。
我如何使用 utPLSQL 检查上面所示结果的种类 — 自动检查吗?测试框架提供各种各样的判定程序。例如,我可以检查两个标量值是否相等,这一点您在 betwnstr 中已经看到:
utassert.eq ('zero start', check_this, against_this);
但是,我还可以进行更有趣的判定。我可以检查结果是否为 NULL.我可以检查两个表、两个查询、两个文件、两个数据库管道或两个集合是否相等。我可以检查是否引发了特定的异常。利用最新版本 (2.0.10.2) 的 utPLSQL(感谢 Rainer Medert的贡献),我甚至可以分析 DBMS_OUTPUT 的结果,看它是否符合我的预期。
有可能使用上述判定检查来测试 Codecheck 吗?我在较早前曾提及,我在编写这种实用工具时的第一个想法是将结果显示到屏幕。(实际上,Codecheck 的一个早期原型使用了这种技术,您可以在 args_analysis.pkg 脚本中找到此原型。)那样我也许使用 utAssert.eqoutput 过程。
在对此考虑了一段时间后,我认为虽然从理论上讲这个判定程序可能有效,但我确实不赞同围绕 DBMS_OUTPUT 构建测试套件。为什么?假如我(或者其他使用并升级 Codecheck 的某个人)决定压缩输出的格式,将会怎样?我将不得不更改单元测试程序包中的代码。其底线是,通过 DBMS_OUTPUT 测试 Codecheck 的正确性使得数据(分析的结果)和表示(将结果显示在屏幕上的方式)之间的界线变得模糊不清。因为这一方法总的来说不太好,所以我要寻找其他方法。
假如结果更加结构化,而不是在屏幕上显示一行文本,则必将能够更加轻易地验证 Codecheck 正确性。让我们重新查看 allargs_test.noparms2 的结果:
allargs_test.noparms2;allargs_test.noparms2 ('abc');allargs_test.noparms2 ('abc', 'def');
这里实际显示的是以上任一调用中的内容,PL/SQL 不能识别我想要运行哪两个 noparms2 过程。您可以在表 3 中看到另一种表示该信息的方法。我明白自己正在对比哪一对超载,并知道在每个超载中哪个序列的参数有歧义。我可以在数据库表或集合中保存这些信息(既有我所期望的控制数据,也有来自运行 Codecheck 的测试数据)。然后,我就可以使用 utAssert.eqTable,检查 Codecheck 对于控制表所分析的结果,该控制表由我建立并填入了我预期的结果。我已发现,通常填充和使用关系表要比集合轻易得多,因此在回顾迄今为止的思路后,让我们继续采用这一思路:
我并不预备依靠 DBMS_OUTPUT 来验证其正确性,但我确实希望将结果显示到屏幕上,以便 Codecheck 的用户可以方便地看到这些结果。究竟这就是该实用工具的全部要点。
我希望 Codecheck 利用那些相同的结果来填充数据库表,这样我就可以全面而精确地测试该实用工具了。
现在应该设计一个数据库表(或者两或三个),将可持续性数据结构的需求(和期望列表)考虑在内:
存储控制和测试数据。不必将这些数据放在同一个表中,实际上我倾向于将其分开。这样治理数据和执行测试将更加轻易。
跟踪 utPLSQL 需要的所有信息,运行其测试并报告结果。
尽量减少在测试程序包中需要编写的与 utPLSQL 集成的代码数量。
我首先从最后一点开始讲起。我在先前曾提及,测试代码数量多于应用程序的情况并不少见。这很好,但我绝对不会介意获得全面测试的同时不必编写、生成或设计数千行代码。有这种可能吗?
实际上,这是我所希望的用于 Codecheck 的测试程序包的样子(伪代码形式):
BEGIN FOR every testcase LOOP Run Codecheck for the testcase scenario Compare test results to control table END LOOP;END;
换言之,我希望改变那种将所有测试方案的逻辑直接硬编码到测试程序包中的方法(ut_betwnstr 是这种方法的一个示例),而代之以软编码方法,利用数据库表中的信息作来驱动测试。