摘自:http://www.ibm.com/developerworks/cn/rational/r-weikw1/index.html

级别: 中级

魏可伟 (weikewei@cn.ibm.com), IBM CSDL软件工程师, IBM

2007 年 9 月 28 日

对数据驱动测试的支持是 Rational Functional Tester(RFT)的重要特性之一。可是另一方面,RFT 所提供的数据池访问方式相对单一,限制了对用户实现较为复杂的测试策略的可能性。本文介绍了如何使用顺序迭代器之外的迭代器灵活访问 RFT 数据池的方法,并提供自定义迭代器供用户实现不同的数据选择策略。

 

1. RFT 与数据驱动测试

数据驱动测试是一项单个脚本使用不同的输入和响应数据重复执行的技术。在数据驱动测试中,自动化测试脚本使用从外部数据源中读取的变量,而不是直接使用文字值(Literal value)。数据驱动测试的优势在于其将数据与测试脚本分离,从而可以在不修改测试脚本的情况下通过更新测试数据完成对测试用例的增加、更改和删除。


图 1: 将数据硬编码到脚本中的测试方式
 将数据硬编码到脚本中的测试方式

图 2: 使用外部数据源的数据驱动测试
 使用外部数据源的数据驱动测试

RFT 中通常使用数据池(Datapool)作为数据驱动测试的数据源。数据池是一系列数据记录的集合。这些记录作为测试脚本回放时的变量输入。RFT 数据池为表格结构(Tabular)。要创建数据池,用户可以通过 RFT 所提供的数据池编辑器直接操作,也可以从 Excel 或者 Rational Test Manager 中导入数据。数据池建立后,将其与 RFT 的测试脚本相关联,被关联的测试脚本就可以通过迭代器读取数据池的记录,从而实现数据驱动测试了。

2. 实例分析——数据驱动测试

为了更好的说明如何使用 RFT 的迭代器,下面我们给出了一个例子。在这个例子中,待验证的是一个计算器程序 Calculator,该程序提供 cal 方法:

public double cal(String expression);

cal 方法接受一个算术表达式作为输入,并返回该表达式的结果。

为了验证该方法,我们设计了分别包含加减乘除运算的表达式作为输入的测试用例。这些用例对应的数据池如图 3。数据池的第一列为测试用例的名称,同时也是数据记录的标识,第二列为输入的表达式,第三列则为预期的结果。

通过这个例子我们可以更好的理解数据驱动测试。基于不同的数据输入,我们可以验证待测试程序是否对各种类型的运算都能给出正确的结果。当需要增加测试用例时(比如增加覆盖括号运算的测试用例),我们不需要更改原测试脚本,只需要在数据池中增加一条新的记录即可。在后面的章节中我们会以此为例介绍使用不同的迭代方式来实现不同数据选择策略的数据驱动测试。


图 3: 为验证计算器程序而设计的数据池
 为验证计算器程序而设计的数据池  

3. RFT 默认支持的迭代器——顺序迭代访问和随机迭代访问

数据池是数据记录的集合,RFT 是通过迭代器对数据池进行访问的。不同的迭代器访问数据池的方式又都有所不同。其中,顺序迭代器(SequentialIterator)和随机迭代器(RandomIterator)是 RFT 所默认支持的,它们提供了最基本的数据池访问方法。无论是顺序迭代器,还是随机迭代器,都迭代数据池中的全部数据记录。他们的区别在于顺序迭代器按照数据记录在数据池中出现的顺序迭代访问,而随机迭代器则采用随机顺序迭代访问。

RFT 提供创建脚本向导设置数据池的访问方式。在向导中,RFT 提供上述两种迭代器给用户选择。

3.1 顺序迭代访问

我们首先介绍如何通过创建脚本向导建立一个顺序访问数据池的测试用例。在创建一个 RFT 测试脚本时,该向导允许用户选择要关联的数据池和数据池中记录的访问顺序(Datapool Record Selection Order),即选择何种迭代器。


图 4: 通过创建脚本向导选择迭代器
 通过创建脚本向导选择迭代器

除了在脚本创建时指定关联的数据池和迭代器,在脚本创建以后,用户还可以在脚本浏览器(Script Explorer)中重新关联脚本到其他数据池,如图 5 所示。


图 5: 关联测试脚本和数据池
 关联测试脚本和数据池

在将测试脚本与数据池关联后,就可以在脚本中实现对数据池的迭代访问了。在下面的实例代码中,迭代器的类型是在创建脚本向导中指定的,因而无需在脚本中显示声明。(已创建的脚本可以在脚本对应的 RFT 定义文件中查看迭代器类型,RFT 定义文件后缀名为 rftdef)。


列表 1: 使用顺序迭代器实现数据驱动测试

            public class SequentialIteratorDemo extends SequentialIteratorDemoHelper
            {
            public void testMain(Object[] args)
            {
            // 初始化计算器程序
            Calculator calculator = new Calculator();
            // 为数据池中每一条记录运行测试
            while (!dpDone())
            {
            // 读取测试用例名称
            String testcase = dpString("Testcase");
            // 读取期待结果
            Double expected = new Double(dpDouble("Result"));
            // 读取表达式并计算实际结果
            Double actual = new Double(calculator.cal(dpString("Expression")));
            // 利用手动验证点进行验证
            vpManual("VP_" + testcase, expected, actual).performTest();
            // 处理下一个测试用例
            dpNext();
            }
            }
            }
            

在这个例子中,测试数据记录被访问的顺序是 Cal1,Cal2,Cal3,Cal4,Cal5,和在数据池中定义的顺序相同。

3.2 随机迭代访问

上面我们介绍了顺序迭代访问,这也是最简单的数据迭代方式。如果数据池中数据记录在顺序上的关联性会对测试结果产生影响时,另一种迭代方式——随机迭代访问就更加合适。因为在上一小节我们使用脚本创建向导选择了迭代器,在本小节我们介绍另一种指定迭代器的方法——用显式声明的方法使用随机迭代器访问数据池。该方法需要显式调用迭代器的构造函数,并通过 dpInitialize() 方法将迭代器与数据池关联起来。


列表 2: 显式调用随机迭代器访问数据池(请注意斜体标出的部分)

            public class RandomIteratorDemo extends RandomIteratorDemoHelper
            {
            public void testMain(Object[] args)
            {
            // 初始化计算器程序
            Calculator calculator = new Calculator();
            //初始化随机迭代器
            RandomIterator iter = new RandomIterator();
            //将其与数据池关联
            iter.dpInitialize(getDatapool());
            // 为数据池中每一条记录运行测试
            while (!iter.dpDone())
            {
            // 读取测试用例名称
            String testcase =iter.dpString("Testcase");
            // 读取期待结果
            Double expected = new Double(iter.dpDouble("Result"));
            // 读取表达式并计算实际结果
            Double actual = new Double(calculator.cal(iter.dpString("Expression")));
            // 利用手动验证点进行验证
            vpManual("VP_" + testcase, expected, actual).performTest();
            // 处理下一个测试用例
            iter.dpNext();
            }
            }
            }
            

在这个例子中,测试数据记录被访问的顺序是不确定的,其中一种可能是 Cal2,Cal3,Cal5,Cal1,Cal4。

4. 选择合适的数据池访问方式

RFT 默认支持的顺序迭代器和随机迭代器提供了数据驱动的基本支持,可无论使用哪种迭代器,数据池中的所有记录都会被访问到。在实际的测试,特别是回归测试过程中,为了提高测试脚本运行的效率,测试人员会制定不同的数据选择策略来执行数据驱动的测试用例。比如,在一次测试执行过后,只选择在本次执行中失败了测试用例对应的数据记录在下一次运行中执行。又如,在每个回归测试周期只选择数据池中的一部分数据进行测试。顺序迭代器和随机迭代器都无法满足这种要求,需要有更为灵活的迭代器才能支持这种数据选择策略。

笔者在这里介绍三种自定义的迭代器,他们分别用于不同的场景,可以满足不同的数据选择策略的需求。要使用这三种迭代器,请首先将附件中的库文件 rftIter.jar 加入您 RFT 项目的构建路径。

4.1 使用范围迭代器(RangeIterator)访问数据池

场景 1 —— 故障恢复

在测试过程中,如果测试脚本在读入数据池中第 N 条记录后出现异常导致失败,在解决或者跳过该问题记录后,较好的数据选择策略是从错误处继续执行而不必将已成功的用例重新执行。在数据池中有大量数据时,这种需求就更为尤为重要。范围迭代器即适用于此场景。


图 6: 从失败位置继续执行的故障恢复场景
从失败位置继续执行的故障恢复场景

范围迭代器允许只访问数据池中某个范围内的数据记录。例如,当测试脚本在读入数据池中第 N 条记录后出现异常导致失败,在下次执行时,可以通过范围迭代器从第 N 条记录开始剩余脚本的执行,而不必重复执行之前的 N-1 条记录。与 RFT 自己所提供的迭代器不同,范围迭代器需要在初始化过程中指定数据池及所要执行记录的起始和结束的序号。


列表 3a: 范围迭代器的初始化方法

            public RangeIterator(IDatapool datapool, int begin);
            

该构造函数将迭代器与 datapool 关联。begin 所指定的是需要被执行的第一条数据记录在数据池中的序号(序号从 1 开始)。


列表 3b: 范围迭代器的初始化方法

            public RangeIterator(IDatapool datapool, int begin, int end);
            

该构造函数将迭代器与 datapool 关联。begin 所指定的是需要被执行的第一条数据记录在数据池中的序号(序号从 1 开始),end 所指定的则是最后一条要被执行的记录。


列表 4: 从数据池中第 3 条记录开始执行脚本的例子(请注意斜体标出的部分)

            public class RangeIteratorDemo extends RangeIteratorDemoHelper
            {
            public void testMain(Object[] args)
            {
            // 初始化计算器程序
            Calculator calculator = new Calculator();
            // 初始化范围迭代器并将其与数据池关联,该迭代器从数据池中第 3 条数据记录开始迭代
            RangeIterator iter = new RangeIterator(getDatapool(), 3);
            // 为数据池中每一条记录运行测试
            while (!iter.dpDone()) {
            // 读取测试用例名称
            String testcase = iter.dpString("Testcase");
            // 读取期待结果
            Double expected = new Double(iter.dpDouble("Result"));
            // 读取表达式并计算实际结果
            Double actual = new Double(calculator.cal(iter.dpString("Expression")));
            |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
            |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
            // 利用手动验证点进行验证
            vpManual("VP_" + testcase, expected, actual).performTest();
            // 处理下一个测试用例
            iter.dpNext();
            }
            }
            }
            

在这个例子中,测试数据记录被访问的顺序是 Cal3,Cal4,Cal5。Cal1 和 Cal2 没有被执行。

4.2 使用数组迭代器(ArrayIterator)访问数据池

场景 2 —— 故障恢复

在测试脚本的运行代价高昂的情况下,另一种测试执行策略是很普遍的——在前一次测试执行过程中失败的测试用例会被记录下来,在下一次执行中,只有这些失败的测试用例才会被执行。数组迭代器即适用于此场景。


图 7: 只执行失败用例的故障恢复场景
只执行失败用例的故障恢复场景

数组迭代器允许只访问数组中所指定的数据记录。例如,在测试脚本的首次运行中,当输入数据为某些记录时,期望结果和实际结果不同。测试脚本的第二次运行需要确认这些导致失败的输入数据。在这种情况下,可以在首次运行时,将导致失败的记录的序号保存下来,再次运行时则通过数组迭代器实现只执行必要测试的需求。


列表 5: 数组迭代器的初始化方法

            public ArrayIterator(IDatapool datapool, int[] records);
            

该构造函数将迭代器与 datapool 关联。数组 records 中指定的是数据记录在数据池中的序号(序号从 1 开始)。在迭代器迭代过程中,仅访问数组中所指定的数据记录。


列表 6: 从数据池中选取记录执行脚本的例子(请注意斜体标出的部分)

            public class ArrayIteratorDemo extends ArrayIteratorDemoHelper
            {
            public void testMain(Object[] args)
            {
            // 初始化计算器程序
            Calculator calculator = new Calculator();
            // 初始化要执行的测试用例列表,只选择测试用例 Cal1,Cal3,Cal5 执行
            int[] records = new int[] {1, 3, 5};
            // 初始化数组迭代器并将其与数据池关联
            ArrayIterator iter = new ArrayIterator(getDatapool(), records);
            // 为数据池中每一条记录运行测试
            while (!iter.dpDone()) {
            // 读取测试用例名称
            String testcase = iter.dpString("Testcase");
            // 读取期待结果
            Double expected = new Double(iter.dpDouble("Result"));
            // 读取表达式并计算实际结果
            Double actual = new Double(calculator.cal(iter.dpString("Expression")));
            |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
            |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
            // 利用手动验证点进行验证
            vpManual("VP_" + testcase, expected, actual).performTest();
            // 处理下一个测试用例
            iter.dpNext();
            }
            }
            }
            

在这个例子中,测试数据记录被访问的顺序是 Cal1,Cal3,Cal5。Cal2 和 Cal4 没有被执行。

4.3 使用条件迭代器(FilteredIterator)访问数据池

和数组迭代器相似,条件迭代器也用于访问列表中所指定的数据记录。不同的是,条件迭代器是通过指定过滤条件而非记录序号。条件迭代器可以支持更灵活的数据选择策略。例如,可以通过在数据池中增加一列对测试记录分组,在不同的执行周期执行不同组的测试记录。从而即提高回归测试的效率又能够保证测试的完备。


图 8: 一种回归测试用例的数据选择策略
一种回归测试用例的数据选择策略

列表 7: 数组迭代器的初始化方法

            public FilteredIterator(IDatapool datapool, String column, String value);
            

该构造函数将迭代器与 datapool 关联。column 指定的是数据池中的哪一列的数据要被作为选取条件,value 指定的是该列数据所要符合的条件。


列表 8: 从数据池中选取 Testcase=Cal4 的记录执行的例子

            public class FilteredIteratorDemo extends FilteredIteratorDemoHelper
            {
            public void testMain(Object[] args)
            {
            // 初始化计算器程序
            Calculator calculator = new Calculator();
            // 初始化条件迭代器并将其与数据池关联,该迭代器只迭代 Testcase=Cal4 的记录
            FilteredIterator iter = new FilteredIterator(getDatapool(), "Testcase", "Cal4");
            |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
            |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
            // 为数据池中每一条记录运行测试
            while (!iter.dpDone()) {
            // 读取测试用例名称
            String testcase = iter.dpString("Testcase");
            // 读取期待结果
            Double expected = new Double(iter.dpDouble("Result"));
            // 读取表达式并计算实际结果
            Double actual = new Double(calculator.cal(iter.dpString("Expression")));
            |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
            |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
            // 利用手动验证点进行验证
            vpManual("VP_" + testcase, expected, actual).performTest();
            // 处理下一个测试用例
            iter.dpNext();
            }
            }
            }
            

在这个例子中,只有数据记录 Cal4 被执行了。

5. 结束语

数据驱动测试在功能测试和压力测试中都有着广泛的应用。但是有效利用数据驱动测试,还需要和有效的数据选择策略结合在一起。使用不同的迭代器访问数据池,将数据选择逻辑与测试用例逻辑分开,使测试脚本清晰易维护,可以大大提高脚本开发和运行的效率。

本文介绍了如何通过 RFT 默认支持的迭代器对数据池中的数据记录进行访问,还提供并介绍了笔者所开发的三种扩展的迭代器——范围迭代器、条件迭代器和数组迭代器,以及他们所适用的不同的场景和数据选择策略的说明,希望能够对广大测试人员实现更灵活的数据驱动测试有所帮助。

 

参考资料

学习

关于作者

魏可伟,IBM CSDL软件工程师,主要工作是对 DB2 for z/OS 的客户端工具 Visual Explain以及 OSC 进行功能测试,对于 Rational 工具、功能测试以及 OID 很有兴趣。本人于北京大学计算机科学技术系获得学士及硕士学位。邮件:weikewei@cn.ibm.com