单元测试本身并不严格限制过程式还是OOP,白盒还是黑盒,因而测试用例的写法具有很大的随意性。一些程序员对于C++/Java/C#等OO语法特性津津乐道,但却没有掌握OOP的基本思想。怎么知道呢?就从编写的单元测试用例就能看出来。单元测试用例的编写可以直接反映一个程序员是否真正理解了什么是过程式编程,什么是OOP。我甚至觉得,如果在面试中要考察面试者对OOP的掌握程度,考察编写单元测试是一种最好的方法。所以,本文打算介绍单元测试中状态验证和行为验证两种不同的方式,并分析其背后的过程式思想和OOP思想。
过程式和状态验证
以机器语言和汇编语言为代表的早期命令式程序设计语言是von Neumann体系结构“存储程序”(Stored Program)思想的直接体现。在命令式程序设计中,用变量表示数据,用语句表示由计算机执行的指令;程序的执行效果体现在语句对变量值的改变上。后来,以C语言为代表的高级语言在此的基础上引入了过程抽象(Procedure Abstraction),通过定义过程/函数/子程序(Procedure/Function/Subroutine)对一系列的语句进行抽象,形成了过程式程序设计。变量和函数这两种基本元素构成了“变量+函数”的二元结构。函数的设计一般采用自顶向下分而治之的方式,大函数套小函数,层层细化。
图1,过程式“变量+函数”的二元结构
过程式程序的单元测试用例多与函数对应,在一个用例中专门测试某一个函数。单元测试的准备工作好包括:设置全局变量和输入变量等非被测函数局部变量的值;检查内容包括:检查函数的返回值,以及非被测函数局部变量的值。我们称这种通过检查非被测函数局部变量值的方式验证函数正确性的单元测试方法为状态验证(State Verification)。
下面我们以一个经典的堆栈(Stack)为例说明在过程式程序中单元测试的基本方法:
/*C语言*/ void test_push(){ Stack *pStack = create_stack();//创建结构体stack push(pStack, 1); ASSERT_EQUAL(1, pStack->items[0]); /*状态验证*/ push(pStack, 2); ASSERT_EQUAL(2, pStack->items[1]); /*状态验证*/ } void test_pop(){ Stack *pStack = create_stack();/*创建结构体stack*/ pStack->size = 2; pStack->items[0] = 1; pStack->items[1] = 2;/*状态准备*/ int item2 = pop(pStack)); ASSERT_EQUAL(2, item2); int item2 = pop(pStack)); ASSERT_EQUAL(2, item2); } |
OOP和行为验证
上面堆栈的例子中,我们注意到push和pop两个函数是由同一组变量而关联起来的,它们共同协作才实现了堆栈的先入后出(FILO)功能。那么,我们能不能提供一种抽象机制,把原先分离的操作关联起来,通过定义一个新的类型形成一个有机整体呢?这就是数据抽象(Data Abstraction)的基本思想,也是OOP的根源。用化学的语言,如果把int, char等基本类型比喻为单质,那么OOP通过数据抽象形成的抽象数据类型(Abstract Data Type)就好像一种化合物。类型(Type)是数学概念,强调语义,类(Class)是C++/Java/C#等OOP语言为定义类型而提供的语法机制。其中,类的封装性(Encapsulation)是其根本特性。相比过程式程序,封装对数据实现了信息隐藏,把“变量+函数”的二元结构变成了对象的一元结构,只允许对象通过public方法与外部通信。
图2,OOP对象的一元结构