论文部分内容阅读
摘要:在软件测试中,单元测试不仅能够优化软件系统设计,还大大简化了功能测试的测试量。但是在一些情况下进行单元测试比较困难,本文引入了模拟对象Mock Object的概念,利用Mock Object进行单位测试,解决了传统单元测试中存在的一些问题。
关键词:软件测试;单元测试;模拟对象
中图分类号:TP311文献标识码:A文章编号:1009-3044(2008)05-00ppp-0c
1 引言
随着极限编程在实际软件开发项目中的推广,越来越多的项目开始采用测试驱动开发作为主要的软件开发方法。单元测试不仅优化了软件系统设计,还大大简化了功能测试的工作量[1]。但是另一方面.更多的项目在开始不久就发现在很多情况下针对一个类编写单元测试比较困难.随着项目的进行,越来越多的代码无法进行单元测试.到最后整个项目无法继续采用测试驱动的方式进行开发。因此,要将测试驱动开发真正在整个项目里贯彻执行,必须有一种方法能够相对容易的解决这些问题。本文将首先讨论了单元测试和无法或很难进行单元测试的情况,然后引入Mock Object的概念,基于Mock Object实现单元测试。接下来讨论在软件开发过程中引入Mock Object对测试和设计的影响。最后简述了Mock Object的局限性。
2 单元测试
2.1 什么是单元测试
单元测试是对程序中的单个子程序或过程进行测试的过程,也就是说,一开始并不是对整个程序进行测试,而是将注意力集中在对构成程序的较小模块的测试上面[2]。单元测试从两个角度进行测试:一是测试数据都是针对程序的功能来设计的黑盒测试;二是针对程序的逻辑结构来设计测试用例的白盒测试。
2.2 单元测试面对的难题
造成针对一个类难以进行单元测试的主要原因是因为这个类依赖于一些其它的难以测试的资源。主要有这三类最主要的资源:数据库,第三方组件和网络硬件资源。下面我们将对这三大类难以测试的资源进行分类讨论。
2.2.1 数据库
现在大部分的软件项目都会采用数据库作为数据存储。常见的开发团队会在每个开发人员的机器上安装一个本地的数据库,每个人针对自己的数据库进行开发调试。这样做的问题是:必须有一种方式同步数据库的设计。如果有一个人修改了数据库schema或者某个存储过程,这个修改必须同步到所有开发者的本地数据库以及测试服务器上。采用敏捷软件开发的很多项目组往往会浪费大量的时间在数据库设计同步上。更严重的是每周都会遇到由于数据库设计不同步,修改冲突导致的问题导致整个项目的中心源码库在Auto Build时失败。每个开发人员都有自己的测试数据,除了上面提到的需要把这些测试数据同步到所有开发机器和测试服务器上外,还面临更重大的问题。因为测试用例需要修改数据库,因此还必须准备一种机制能够在每一个测试用例执行结束后重新将所有的测试数据调入数据库。采用最简单直接的方法就是在每个测试用例执行前都将数据库清空,然后再将测试数据调入,这样会大大减慢单元测试的时间。单元测试时间越长,开发者就越不愿意执行这些测试用例,单元测试所发挥的作用越小,这也是很多测试驱动项目最终无法进行到底的一个重要原因。另一个非常严重的问题是为了清理测试环境,在针对商业逻辑的测试用例中加入了大量的数据访问层的代码。采用这样的方式强迫开发者在开发商业逻辑层的同时开发数据访问层,并且严重降低了可读性。
2.2.2 第三方组件或应用服务器
数据库是最常见的第三方服务器。除此以外在越来越多的项目中使用第三方的组件和应用服务器。例如:客户环境中的ERP系统,全球定位系统(GPS)的Web Service接口,绘图引擎等。对于这些第三方提供的内容,造成难以编写单元测试的最根本的原因有:一是系统不透明:对于大部分商业组件或者服务来说,一个很重要的内容是良好的封装。但这个特性带来的问题是在外界无法对其内部状态进行控制和访问。往往经过好几个操作后才能在外部观察到相应的变化。二是环境配置困难。由于项目组成员计算机配置不同,加入项目的时间不同,在项目中负责的内容不同导致无法为所有开发人员配置一个完全一致的环境。例如一个绘图引擎的开发版的license是按照一个局域网内部同时使用的人员个数收费的,就不可能只为了能够进行完整的单元测试就为只编写商业逻辑层的开发人员也安装一套。
2.2.3 网络资源和硬件资源
在稍大一些的项目中都或多或少的用到一些网络资源。例如将文件部署到远程的webDAV服务器上同时很多项目还会用到一些硬件资源。常见的有打印机、指纹识别验证或者条形码阅读器等。这些资源有两大特点导致很难针对与他们相关的类编写测试用例。
一是资源访问冲突。很多网络资源对于并发访问的响应协调是通过锁机制进行的,在实际项目中常见的是一个开发人员在调试本地代码时导致远端资源被锁定导致其它开发者无法访问这些资源。
二是环境可控因素。对于网络资源和硬件资源相关代码的测试与针对商业逻辑层代码的测试最大的不同是环境的不确定性。访问网络资源有可能遇到的异常情况非常多,例如网络忙造成访问超时,也有可能建立链接后数据传输失败,还有可能数据传输完成后校验失败。针对访问这些资源的代码进行的测试必须能够覆盖到所有可能出现的每一种情况。如果没有一个可控,并且是全自动的环境辅助单元测试的话,这项任务基本上不可能完成。
3 模拟对象
3.1 什么是模拟对象
Mock这个单词翻译成中文大概的意思是假的,模拟的。如图1所示:通过一个常见的对商业逻辑的测试描述了一个Mock Object。在图中我们可以看出:测试代码需要测试商业逻辑,而商业逻辑代码需要通过IMyDataAccess接口访问底层数据库,这就是数据库依赖问题。为了解决这个问题我们引入一个Mock Object,并将这个Mock Object而非真正的Data Access传递给商业逻辑代码进行测试。这里的Mock Object不需要实现任何逻辑只需要根据商业逻辑的需要返回适当的内容就可以了。
图1 使用Mock Object对商业逻辑进行测试
3.2 模拟对象实现单元测试应用实例
现在我们写好了类AccountService,具体如下:
public class AccountService {
private AccountManager accountManager;
public void setAccountManager(AccountManager manager) {
this.accountManager = manager;
}
public void transfer(String senderId, String beneficiaryId, long amount) {
Account sender = this.accountManager.findAccountForUser(senderId);
Account beneficiary =
this.accountManager.findAccountForUser(beneficiaryId);
sender.debit(amount);
beneficiary.credit(amount);
this.accountManager.updateAccount(sender);
this.accountManager.updateAccount(beneficiary);
}}
现在我们想测试transfer方法,它内部调用的AccountManager的两个方法。但是对于AccountManager来说,它只是个接口,如下:
public interface AccountManager {
Account findAccountForUser(String userId);
void updateAccount(Account account);
}
所以现在我们必须写个MockAccountManager对象。而且里面的方法体都是非常简单的,就是假定它就返回某某值。
我们这里还有Account类。
public class Account {
private String accountId;
private long balance;
public Account(String accountId, long initialBalance) {
this.accountId = accountId;
this.balance = initialBalance;
}public void debit(long amount) {
this.balance -= amount;
}
public void credit(long amount) {
this.balance = amount;
}
public long getBalance() {
return this.balance;
}
public String getAccountId() {
return accountId;
}}
public class AccountService1Tests extends TestCase {
public void testTransfer(){
AccountService as = new AccountService();
MockAccountManager mockAccountManager=new MockAccountManager();
Account accountA = new Account("A",3000);
Account accountB = new Account("B",2000);
mockAccountManager.addAccount(accountA);
mockAccountManager.addAccount(accountB);
as.setAccountManager(mockAccountManager);
as.transfer("A","B",1005);
assertEquals(accountA.getBalance(),1995);
assertEquals(accountB.getBalance(),3005);
}}
这里我们在假定AccountManager方法都工作正常的情况下,完成了对transfer方法的测试。
从以上代码可以看出,采用Mock Object进行的单元测试基本上可以分为下面几步:
(1)基于一个接口定义Mock并实现这个接口的所有函数。
(2)创建Mock Object的一个对象
(3)设置对象内部属性
(4)告诉对象测试代码希望看到的反应
(5)进行测试
(6)检查Mock Object的确按照希望的顺序进行工作。
3.3 模拟对象的优点
3.3.1 模拟对象作为测试手段的优点
Mock Object最直接的优点在于提供单元测试的质量和覆盖率:
(1)只要在测试中对期待发生的问题指定好执行的顺序引入Mock Object对象后的单元测试就是在一个完全可控的环境里进行的。也就是说我们再也不会无法定位一个“时隐时现”的bug。相反我们可以非常迅速的将问题定位在一个类的内部,而不是一个函数调用序列。
(2)于测试人员来说,最常见的问题是测试人员提交的bug无法在开发人员那里复现。有了Mock Object这个工具测试人员可以利用Mock Object明确的指定输入和输出编写一个测试用例让开发人员修复。
(3)超过8O% 的异常处理代码没有被充分测试过。主要原因是在没有Mock Object之前很多情况是无法由人工进行控制的,例如写文件失败网络连接超时,数据库数据传输失败或者从网络接收到的数据已经损坏。通过控制Mock Object我们很容易就可以模拟上面的这些情况。
3.3.2 模拟对象作为设计手段的优点
虽然Mock Object最直接的优点在于给予测试代码更多的可控性和可操作性,它最大的优点在于对软件设计的影响[3]。
(1)测试驱动开发与Mock Object一起使用,可以写出低耦合高内聚,非常优雅干净的代码。
(2)强迫设计者放弃对第三方库的强依赖关系,取而代之的是比较弱的依赖关系。
(3)设计人员可以将更大的注意力放在商业逻辑的实现和测试.由于Mock Object的存在,我们不需要实现数据访问层就可以对商业逻辑进行测试。而商业逻辑才是任何系统中对于客户最重要的内容,它的正确与否决定了整个系统是否能完成任务,它的稳定性决定了整个系统架构的稳定性。
(4)在项目初期,甚至是中期,将设计人员解放出来,不用对系统底层的基础设施做出判断。例如,在商业逻辑并不明确,需求还不稳定的时候,我们是更多根据感觉来做出很多重要的判断的,而这些判断往往导致比较关键的决定。例如,在项目之初,谁能够明确的回答到底需要什么样的数据库?Oracle?SQL Server?还是XML文件?到底需要什么样的队列服务器 MSMQ还是IBM—MQ?由于Mock Object的引入,我们可以将这些决策推迟到商业逻辑层更加明确之后进行,从而可以获得更加准确有针对性的答案。
3.4 模拟对象的局限性
Mock Object在实际项目中的应用存在一些限制,一些是由于Mock Object本身性质决定的,有一些则是由于其它类库设计存在的缺陷导致的。
(1)一个典型的不是Mock Object的问题,而是类库设计的问题。是Mock Object无法模拟比较深的对象树。有一些第三方的类库,尤其是一些消息处理函数的参数,提供的不是接口而是一些对象。往往这些对象内部有很多子对象,也就是我们常说的一棵大的对象树。我们需要花费太多的精力去构造这些对象来进行模拟,时间消耗巨大。
(2)一般性而言,单元测试的粒度越细,功能测试的粒度就可以越粗[4]。但是引入Mock Object的单元测试仍然无法取代功能测试。一个很好的例子就是误差积累的测试,哪怕每个单元的误差都在可接收范围内,我们仍然需要一个功能测试确保整体误差也是可以接受的。
4 结束语
模拟对象解决了传统单元测试的两个问题:一是如何将需要测试的代码与相关环境隔离;二是如何创建一个快速、可控的环境辅助测试开发。随着模拟对象技术的成熟,基于模拟对象的单元测试会越来越广泛地被采用。
参考文献:
[1]Kent Beck.测试驱动开发[M].北京:中国电力出版社,2003.
[2]Myers.王峰,陈杰译.软件测试的艺术(第二版)[M]. 北京:机械工业出版社,2006.50-52.
[3]David Astels.崔凯,译.测试驱动开发实用指南[M].北京:中国电力出版社,2004.120-130.
[4]Paul C Jorgensen.韩柯,杜旭涛,译.软件测试(第二版)[M].北京:机械工业出版社,2003.
收稿日期:2007-12-15
作者简介:刘赟(1980-),男,硕士,研究方向:软件工程。
关键词:软件测试;单元测试;模拟对象
中图分类号:TP311文献标识码:A文章编号:1009-3044(2008)05-00ppp-0c
1 引言
随着极限编程在实际软件开发项目中的推广,越来越多的项目开始采用测试驱动开发作为主要的软件开发方法。单元测试不仅优化了软件系统设计,还大大简化了功能测试的工作量[1]。但是另一方面.更多的项目在开始不久就发现在很多情况下针对一个类编写单元测试比较困难.随着项目的进行,越来越多的代码无法进行单元测试.到最后整个项目无法继续采用测试驱动的方式进行开发。因此,要将测试驱动开发真正在整个项目里贯彻执行,必须有一种方法能够相对容易的解决这些问题。本文将首先讨论了单元测试和无法或很难进行单元测试的情况,然后引入Mock Object的概念,基于Mock Object实现单元测试。接下来讨论在软件开发过程中引入Mock Object对测试和设计的影响。最后简述了Mock Object的局限性。
2 单元测试
2.1 什么是单元测试
单元测试是对程序中的单个子程序或过程进行测试的过程,也就是说,一开始并不是对整个程序进行测试,而是将注意力集中在对构成程序的较小模块的测试上面[2]。单元测试从两个角度进行测试:一是测试数据都是针对程序的功能来设计的黑盒测试;二是针对程序的逻辑结构来设计测试用例的白盒测试。
2.2 单元测试面对的难题
造成针对一个类难以进行单元测试的主要原因是因为这个类依赖于一些其它的难以测试的资源。主要有这三类最主要的资源:数据库,第三方组件和网络硬件资源。下面我们将对这三大类难以测试的资源进行分类讨论。
2.2.1 数据库
现在大部分的软件项目都会采用数据库作为数据存储。常见的开发团队会在每个开发人员的机器上安装一个本地的数据库,每个人针对自己的数据库进行开发调试。这样做的问题是:必须有一种方式同步数据库的设计。如果有一个人修改了数据库schema或者某个存储过程,这个修改必须同步到所有开发者的本地数据库以及测试服务器上。采用敏捷软件开发的很多项目组往往会浪费大量的时间在数据库设计同步上。更严重的是每周都会遇到由于数据库设计不同步,修改冲突导致的问题导致整个项目的中心源码库在Auto Build时失败。每个开发人员都有自己的测试数据,除了上面提到的需要把这些测试数据同步到所有开发机器和测试服务器上外,还面临更重大的问题。因为测试用例需要修改数据库,因此还必须准备一种机制能够在每一个测试用例执行结束后重新将所有的测试数据调入数据库。采用最简单直接的方法就是在每个测试用例执行前都将数据库清空,然后再将测试数据调入,这样会大大减慢单元测试的时间。单元测试时间越长,开发者就越不愿意执行这些测试用例,单元测试所发挥的作用越小,这也是很多测试驱动项目最终无法进行到底的一个重要原因。另一个非常严重的问题是为了清理测试环境,在针对商业逻辑的测试用例中加入了大量的数据访问层的代码。采用这样的方式强迫开发者在开发商业逻辑层的同时开发数据访问层,并且严重降低了可读性。
2.2.2 第三方组件或应用服务器
数据库是最常见的第三方服务器。除此以外在越来越多的项目中使用第三方的组件和应用服务器。例如:客户环境中的ERP系统,全球定位系统(GPS)的Web Service接口,绘图引擎等。对于这些第三方提供的内容,造成难以编写单元测试的最根本的原因有:一是系统不透明:对于大部分商业组件或者服务来说,一个很重要的内容是良好的封装。但这个特性带来的问题是在外界无法对其内部状态进行控制和访问。往往经过好几个操作后才能在外部观察到相应的变化。二是环境配置困难。由于项目组成员计算机配置不同,加入项目的时间不同,在项目中负责的内容不同导致无法为所有开发人员配置一个完全一致的环境。例如一个绘图引擎的开发版的license是按照一个局域网内部同时使用的人员个数收费的,就不可能只为了能够进行完整的单元测试就为只编写商业逻辑层的开发人员也安装一套。
2.2.3 网络资源和硬件资源
在稍大一些的项目中都或多或少的用到一些网络资源。例如将文件部署到远程的webDAV服务器上同时很多项目还会用到一些硬件资源。常见的有打印机、指纹识别验证或者条形码阅读器等。这些资源有两大特点导致很难针对与他们相关的类编写测试用例。
一是资源访问冲突。很多网络资源对于并发访问的响应协调是通过锁机制进行的,在实际项目中常见的是一个开发人员在调试本地代码时导致远端资源被锁定导致其它开发者无法访问这些资源。
二是环境可控因素。对于网络资源和硬件资源相关代码的测试与针对商业逻辑层代码的测试最大的不同是环境的不确定性。访问网络资源有可能遇到的异常情况非常多,例如网络忙造成访问超时,也有可能建立链接后数据传输失败,还有可能数据传输完成后校验失败。针对访问这些资源的代码进行的测试必须能够覆盖到所有可能出现的每一种情况。如果没有一个可控,并且是全自动的环境辅助单元测试的话,这项任务基本上不可能完成。
3 模拟对象
3.1 什么是模拟对象
Mock这个单词翻译成中文大概的意思是假的,模拟的。如图1所示:通过一个常见的对商业逻辑的测试描述了一个Mock Object。在图中我们可以看出:测试代码需要测试商业逻辑,而商业逻辑代码需要通过IMyDataAccess接口访问底层数据库,这就是数据库依赖问题。为了解决这个问题我们引入一个Mock Object,并将这个Mock Object而非真正的Data Access传递给商业逻辑代码进行测试。这里的Mock Object不需要实现任何逻辑只需要根据商业逻辑的需要返回适当的内容就可以了。
图1 使用Mock Object对商业逻辑进行测试
3.2 模拟对象实现单元测试应用实例
现在我们写好了类AccountService,具体如下:
public class AccountService {
private AccountManager accountManager;
public void setAccountManager(AccountManager manager) {
this.accountManager = manager;
}
public void transfer(String senderId, String beneficiaryId, long amount) {
Account sender = this.accountManager.findAccountForUser(senderId);
Account beneficiary =
this.accountManager.findAccountForUser(beneficiaryId);
sender.debit(amount);
beneficiary.credit(amount);
this.accountManager.updateAccount(sender);
this.accountManager.updateAccount(beneficiary);
}}
现在我们想测试transfer方法,它内部调用的AccountManager的两个方法。但是对于AccountManager来说,它只是个接口,如下:
public interface AccountManager {
Account findAccountForUser(String userId);
void updateAccount(Account account);
}
所以现在我们必须写个MockAccountManager对象。而且里面的方法体都是非常简单的,就是假定它就返回某某值。
我们这里还有Account类。
public class Account {
private String accountId;
private long balance;
public Account(String accountId, long initialBalance) {
this.accountId = accountId;
this.balance = initialBalance;
}public void debit(long amount) {
this.balance -= amount;
}
public void credit(long amount) {
this.balance = amount;
}
public long getBalance() {
return this.balance;
}
public String getAccountId() {
return accountId;
}}
public class AccountService1Tests extends TestCase {
public void testTransfer(){
AccountService as = new AccountService();
MockAccountManager mockAccountManager=new MockAccountManager();
Account accountA = new Account("A",3000);
Account accountB = new Account("B",2000);
mockAccountManager.addAccount(accountA);
mockAccountManager.addAccount(accountB);
as.setAccountManager(mockAccountManager);
as.transfer("A","B",1005);
assertEquals(accountA.getBalance(),1995);
assertEquals(accountB.getBalance(),3005);
}}
这里我们在假定AccountManager方法都工作正常的情况下,完成了对transfer方法的测试。
从以上代码可以看出,采用Mock Object进行的单元测试基本上可以分为下面几步:
(1)基于一个接口定义Mock并实现这个接口的所有函数。
(2)创建Mock Object的一个对象
(3)设置对象内部属性
(4)告诉对象测试代码希望看到的反应
(5)进行测试
(6)检查Mock Object的确按照希望的顺序进行工作。
3.3 模拟对象的优点
3.3.1 模拟对象作为测试手段的优点
Mock Object最直接的优点在于提供单元测试的质量和覆盖率:
(1)只要在测试中对期待发生的问题指定好执行的顺序引入Mock Object对象后的单元测试就是在一个完全可控的环境里进行的。也就是说我们再也不会无法定位一个“时隐时现”的bug。相反我们可以非常迅速的将问题定位在一个类的内部,而不是一个函数调用序列。
(2)于测试人员来说,最常见的问题是测试人员提交的bug无法在开发人员那里复现。有了Mock Object这个工具测试人员可以利用Mock Object明确的指定输入和输出编写一个测试用例让开发人员修复。
(3)超过8O% 的异常处理代码没有被充分测试过。主要原因是在没有Mock Object之前很多情况是无法由人工进行控制的,例如写文件失败网络连接超时,数据库数据传输失败或者从网络接收到的数据已经损坏。通过控制Mock Object我们很容易就可以模拟上面的这些情况。
3.3.2 模拟对象作为设计手段的优点
虽然Mock Object最直接的优点在于给予测试代码更多的可控性和可操作性,它最大的优点在于对软件设计的影响[3]。
(1)测试驱动开发与Mock Object一起使用,可以写出低耦合高内聚,非常优雅干净的代码。
(2)强迫设计者放弃对第三方库的强依赖关系,取而代之的是比较弱的依赖关系。
(3)设计人员可以将更大的注意力放在商业逻辑的实现和测试.由于Mock Object的存在,我们不需要实现数据访问层就可以对商业逻辑进行测试。而商业逻辑才是任何系统中对于客户最重要的内容,它的正确与否决定了整个系统是否能完成任务,它的稳定性决定了整个系统架构的稳定性。
(4)在项目初期,甚至是中期,将设计人员解放出来,不用对系统底层的基础设施做出判断。例如,在商业逻辑并不明确,需求还不稳定的时候,我们是更多根据感觉来做出很多重要的判断的,而这些判断往往导致比较关键的决定。例如,在项目之初,谁能够明确的回答到底需要什么样的数据库?Oracle?SQL Server?还是XML文件?到底需要什么样的队列服务器 MSMQ还是IBM—MQ?由于Mock Object的引入,我们可以将这些决策推迟到商业逻辑层更加明确之后进行,从而可以获得更加准确有针对性的答案。
3.4 模拟对象的局限性
Mock Object在实际项目中的应用存在一些限制,一些是由于Mock Object本身性质决定的,有一些则是由于其它类库设计存在的缺陷导致的。
(1)一个典型的不是Mock Object的问题,而是类库设计的问题。是Mock Object无法模拟比较深的对象树。有一些第三方的类库,尤其是一些消息处理函数的参数,提供的不是接口而是一些对象。往往这些对象内部有很多子对象,也就是我们常说的一棵大的对象树。我们需要花费太多的精力去构造这些对象来进行模拟,时间消耗巨大。
(2)一般性而言,单元测试的粒度越细,功能测试的粒度就可以越粗[4]。但是引入Mock Object的单元测试仍然无法取代功能测试。一个很好的例子就是误差积累的测试,哪怕每个单元的误差都在可接收范围内,我们仍然需要一个功能测试确保整体误差也是可以接受的。
4 结束语
模拟对象解决了传统单元测试的两个问题:一是如何将需要测试的代码与相关环境隔离;二是如何创建一个快速、可控的环境辅助测试开发。随着模拟对象技术的成熟,基于模拟对象的单元测试会越来越广泛地被采用。
参考文献:
[1]Kent Beck.测试驱动开发[M].北京:中国电力出版社,2003.
[2]Myers.王峰,陈杰译.软件测试的艺术(第二版)[M]. 北京:机械工业出版社,2006.50-52.
[3]David Astels.崔凯,译.测试驱动开发实用指南[M].北京:中国电力出版社,2004.120-130.
[4]Paul C Jorgensen.韩柯,杜旭涛,译.软件测试(第二版)[M].北京:机械工业出版社,2003.
收稿日期:2007-12-15
作者简介:刘赟(1980-),男,硕士,研究方向:软件工程。