接口调用定律与测试驱动接口设计
2.5 测试驱动接口设计
2.5.1 接口调用定律与测试驱动接口设计
敏捷软件开发的原则里,有一条原则是朝着稳定的方向依赖的原则,在作者的博客里有一篇文章探讨了接口关系稳定原理,主要的意思是应该让接口稳定性低的模块调用接口稳定性高的模块。有关接口关系稳定原理的详细情况请看作者博客http://blog.csdn.net/ drzhouweiming。
在面向对象的思想里,一个重要的思想就是数据的封装与隐藏,也就是说一个模块要操作另外一个模块时需要使用它的接口来操作它的内部数据,而不能绕过接口直接去操作它的内部数据。
面向对象的这个思想,其实可以由接口关系稳定原理推导出来:
接口覆盖定理:一个模块必须通过另外一个模块的稳定接口去操作(使用)它的可变数据。换句话说,一个模块的稳定接口集合必须覆盖所有其他模块需要操作的可变数据。
如果一个模块直接操作另一个模块的可变数据,相当于使用可变数据作为接口,由于可变数据存在变化的可能,因此接口是不稳定的,这和接口关系稳定原理产生冲突,因此一个模块不能直接去操作另一模块的可变数据,而需要通过另一模块的抽象接口来操作它的可变数据。
关于接口覆盖定理,最常遇到的情况就是类的成员变量要提供Get()、Set()方法来进行操作,不能直接去访问类的成员变量。
根据接口覆盖定理,外部模块操作模块内部的可变数据时,必须由模块提供的稳定接口来进行操作。设计模块的稳定接口时,必须知道模块有哪些可变数据,而可变数据的寻找工作属于测试的范畴,因此稳定接口的设计可以通过测试来驱动。
一个模块的接口实际上就是对模块的可变数据进行解释的方法,可变数据的变化本质上都是由外部原因引起的,模块接口层的可变数据是由外部输入层的可变数据驱动的,因此设计接口时,首先需要分析外部输入层的可变数据。
2.5.2 测试驱动设计的步骤
采用测试驱动设计时,应按以下步骤来进行。
(1)找出可变数据,根据测试数据构造出测试空间。
(2)将测试空间抽象成编程语言的基本数据类型或由基本数据类型组合成的数据结构类型。其实这和《设计模式解析》一书中所说的根据可变数据的共性创建抽象是一回事。
(3)如果需要给外部模块提供输出结果,那么还需要分析测试空间中的对应输出结果集合,将结果集合也抽象成编程语言的基本数据类型或由基本数据类型组合成的数据结构类型。
(4)分析其他模块对本模块的需求,根据需求分解出各个操作,合并共性的操作,得出操作集合。
(5)分析各个操作所设计的可变数据,设计出对应接口来处理抽象的数据类型,将抽象的数据类型作为接口的输入和输出参数。
(6)检查是否有遗漏未被接口覆盖的可变数据,如果有,表明接口有遗漏需要重新分析需求中涉及的操作。
下面以文件操作的接口设计为例讲解测试驱动设计。为方便讲解起见,假设需求中只考虑文件的二进制读写需求,不考虑文件的其他操作需求。
读操作中,牵涉到的可变数据有文件名、文件系统数据(文件头等)、文件内容、要读取数据的文件位置和读取数据的长度等。读操作需要将读取的数据输出给其他模块使用,因此可能还需要做一些校验。
写操作中,牵涉到的可变数据有文件名、文件系统数据、写入文件的数据、写入数据的长度和写入的位置等。
文件名抽象成一个字符串或字符串指针,所需用到的文件系统数据可以抽象成一个文件描述符,文件位置抽象成一个整型变量,长度也可抽象成一个整型变量,写入数据可抽象成一个void指针。
读操作的处理过程需要先从文件系统中找到对应文件,然后读取文件头数据进行分析,再从文件中读取指定位置的数据,最后将处理过程中所需的内存等资源释放掉。写操作的处理过程和读操作类似,不同的只是将读数据改为写数据,另外如果找不到文件时可能需要创建一个空文件。
根据处理过程中所涉及的操作,可以设计出4个基本操作来满足上面的处理过程:
(1)打开文件操作:负责从操作系统中找到对应文件或创建空文件,读取文件头数据进行分析,打开文件操作需要,输出文件描述符。
(2)读文件操作:负责从文件的指定位置读取指定长度的数据,输出读取的数据,并输出实际读取的长度和读取是否成功的信息。
(3)写文件操作:负责将指定长度的数据写入文件的指定位置,需要输出写入是否成功的信息。
(4)关闭操作:释放处理过程中分配的各种资源。
这样在将操作分解完后,打开文件操作涉及的可变数据为文件名和文件头数据等。
读文件操作涉及的可变数据有文件描述符、文件位置、读取长度。
写文件操作涉及的可变数据有文件描述符、文件位置、写入数据、写入数据的长度。
关闭操作涉及的可变数据主要是文件描述符。
这样可以设计出如下4个基本接口:
u int FileRead(HANDLE hFile, int nPos, void *pBuf, int nReadLen);
u HANDLE FileOpen(char *pszFileName);
u int FileWrite(HANDLE hFile, int nPos, void *pData, int nDataLen);
u void FileClose(HANDLE hFile);
当然,这里也可以像C标准函数一样设计fseek()函数来操作nPos可变数据,FileRead和FileWrite接口中就不需要nPos参数了,接口使用起来更符合使用要求。
2.5.3 命令行程序接口设计实例
为了加深对测试驱动设计的理解,这里再讲一个命令行程序接口设计实例。以一个Unix命令行程序为例,Unix提供了许多的命令如ls、cd、mkdir、ps等命令供用户使用,有些命令属于内部命令,有些属于外部命令,如何来设计命令行程序提供给其他模块使用的接口呢?
在命令行程序中,外部输入的可变数据就是用户在命令行中输入的命令和参数字符串,它的测试空间是用户输入的各种可能的字符串集合。因此可以用一个字符串指针来表示用户输入的可变数据。
当然命令行程序中的可变数据并不只是用户在命令行中输入的数据,它还包括各种命令,由于各种命令在未来的操作系统版本中可能发生变化,因此这些命令的程序执行代码对命令行程序来说也是可变数据。例如ls、cd等内部命令,将来可能会增加或修改内部命令,这些内部命令的函数实体是可变的,所构成的测试空间为一系列函数实体的集合,对于这些可变数据,可以将其抽象成一个可查找的数据结构,数据结构的每个节点里包含一个指向函数实体的函数指针和命令名字。
因此命令行程序里需要提供操作用户输入字符串及不同命令函数实体可变数据的接口。根据命令行程序的功能,一个接口用来执行命令,操作用户输入的字符串可变数据。另外一个接口用来操作不同命令函数实体可变数据,用来将命令注册到内部可变数据的抽象上。
这样就可以设计出命令程序的两个接口——
n 执行接口
int Execute(char *pszCmdLine);
n 命令注册接口
typedef int (*RUNCMD)(char *pszCmdLine);
int RegCmd(char *pszCmdName, RUNCMD RunCmdFunc);
如果使用类作为接口,那么将这两个接口放入类中,并用虚函数的多态性来取代回调函数,就可以看到它和设计模式中的命令模式是一致的。由此也可以发现设计模式实质上是对可变数据创建抽象的一些具体方法。