您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
使用 Python 在 Linux 上实现一键回归测试
 
作者 丁朝杰,涂波,赵亚丽,火龙果软件    发布于 2014-06-12
 

在 Linux 平台上进行大型项目开发过程中,测试人员需要定期(通常是每天),从代码库中更新代码、编译版本、运行全部测试脚本、收集并发布测试结果。这个过程既繁琐又耗时。通常我们希望能在下班后能自动完成这一系列操作,本文将讲述如何利用 Python 脚本轻松实现这一过程。

从代码库迁出代码 ---- pexpect 的使用

测试人员从代码库(例如 CVS )迁出代码的过程中,需要手动输入访问密码,而 Python 提供了 Pexpect 模块则能够将手动输入密码这一过程自动化。当然 Pexpect 也可以用来和 ssh、ftp、passwd、telnet 等命令行进行自动化交互。这里我们以 CVS 为例展示如何利用 Pexpect 从代码库迁出代码。

清单 1. 用 pexpect 迁出代码库代码

try:
chkout_cmd = 'cvs co project_code' #从代码库迁出 project_code 的内容
child = pexpect.spawn(chkout_cmd)
child.expect('password:')
child.sendline('your-password') #请替换"your-password"为真实密码
child.interact()
except:
pass #忽略迁出代码中的错误

在清单 1 中,我们用命令"cvs co project_code"从代码库中迁出了 project_code 的内容,我们也可以用该命令来更新已经迁出的代码。只需要将命令"cvs update" 传给类 pexpect.spawn()即可,详细的实现请参考代码文件。这里 interact()函数是必须的,用来在交互的方式下控制该子进程。有时代码库中会存在目录不一致行情况,迁出代码会因报错终止,所以需要异常处理(try ... execpt)来忽略该错误。

编译代码和运行测试脚本 ---- subprocess 的使用

测试人员获取最新的代码之后,就要对源码进行编译,并且运行测试用例。Python 语言提供了多种方法如 os.system()/os.popen()来执行一条命令,这里我们推荐用 subprocess 模块来创建子进程,完成代码编译和运行测试用例。因为 subprocess 支持主进程和子进程的交互,同时也支持主进程和子进程是同步执行还是异步执行。由于本文中的各个功能模块有都先后依赖关系,所以全部采用的是主进程和子进程同步模式执行。

编译代码

清单 2. 用 subprocess 编译代码

build_cmd = 'build_command_for_your_code' #请在这里配置编译命令
build_proc = subprocess.Popen(build_cmd, stdin=None, stdout=None, stderr=None, shell=True)
build_proc.wait() #等待子进程结束
assert (0 == build_proc.returncode)

在一些系统中我们编译代码采用的是脚本文件(如 shell 脚本),那么我们仍然可以如下命令来完成代码编译工作。

清单 3. 用 subprocess 的 call 函数执行脚本文件

 subprocess.call(["code_compile.sh"])

运行测试脚本

在编译完成代码之后,我们同样可以调用 subprocess.Popen 来创建子进程运行测试用例。如果测试人员的测试用例已经写成了测试例脚本,我们则可以用 subprocess.call()来执行测试例脚本文件,代码实现就不再赘述。有些系统会直接把详细日志输出到屏幕上,那么我们可以用重定向命令"2>&1"把屏幕输出写文件。

清单 4. 用重定向命令把输出写文件

ut_cmd = 'Your_unit_test_command  2>&1 > 
%s' %self.debug_log #debug_log 定义在__init__函数中,用来存储详细日志

测试结果存储和发布 ---- XML 解析

我们的项目采用敏捷开发,为了更好的反应敏捷开发周期,我们希望存储日志的目录名不但能够指明的具体日期,同时也能反映敏捷(迭代)开发阶段,这样相关人员在查看相应目录中的日志时,能够清楚的明白日志实在在哪个迭代周期的哪一天产生的。本文使用文件 summary 作为运行测试用例后生成的汇总日志,用文件 log.txt 用来存储详细日志。如下图所示,在共享目录 SharedFiles 中存储了一些列迭代周期中的日志。

清单 5. 共享目录结构

SharedFiles
├── Sprint10-20130823121500
│ ├── log.txt
│ └── summary
├── Sprint10-20130826152715
│ ├── log.txt
│ └── summary
├── Sprint10-20130828165235

为了能够让目录名反映敏捷开发周期,我们需要自己定义一个配置文件(txt 或 xml 均可)。由于 Python 已经很好的支持了 XML 解析,并且 XML 文件作为配置也是当前的流行趋势。本文就以 XML 解析为例进行说明。本文使用的 XML 文件名是 Sprint.xml,清单 6 是该 xml 的概要内容

清单 6. Sprint.xml 文件结构

<sprint-schedule>
<min-sprint>10</min-sprint>
<max-sprint>20</max-sprint>
<sprint10>20130814</sprint10>
<sprint11>20130828</sprint11>
… …
<sprint19>20131218</sprint19>
<sprint20>20140101</sprint20>
</sprint-schedule>

关于 xml 解析 Python 提供了多种方法。本文采用 minidom 对 xml 文件进行解析,清单 7 是相关处理代码。

清单 7. xml 解析代码

cur_date = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time())) # 首先获取当前系统日期

xmldoc = minidom.parse(xml_file)
min_num_node = xmldoc.getElementsByTagName('min-sprint')[0]
min_num = int(min_num_node.firstChild.data) #解析出迭代开发周期的起始周期

max_num_node = xmldoc.getElementsByTagName('max-sprint')[0]
max_num = int(max_num_node.firstChild.data) #解析出迭代开发周期的终止周期

cur_num = min_num
#遍历所有迭代周期,取出当前迭代周期的开始时间和当前的系统时间对比,从而确定当前位于哪一个迭代周期。
while cur_num <= max_num :
node_name = 'sprint' + str(cur_num)
cur_node = xmldoc.getElementsByTagName(node_name)[0]
sprint_date = cur_node.firstChild.data
if sprint_date < cur_date[0:7]:
cur_num = cur_num + 1
else:
break

这样 cur_num 就指向了当前的迭代开发周期。然后,我们就可以根据当前日期和开发阶段创建对应的日志目录名了,最后把运行结果存储到该目录下,参见清单 8 实现。

清单 8. 日志存储代码

log_dir = self.share_dir + '/Sprint' + str(cur_num) + '-' + cur_date #share_dir 为共享目录,定义在初始化函数中
os.mkdir(log_dir)
os.system('mv %s %s' %(self.debug_fullname, log_dir)) #debug_fullname,详细日志文件名(含目录),定义在初始化函数中
os.system('mv %s %s' %(self.sum_fullname, log_dir)) #sum_fullname,汇总日志的全路径文件名,定义在初始化函数中

关于测试结果的发布,本文并没有把测试结果以自动化的形式发送邮件,而是手动在每个开发周期结束时,群发邮件给相关人员。或者在验证失败后,通知相关的开发人员,这是由于作者所在团队项目代码提交频率不是很高。在更大型的项目中,往往需要增加自动发送邮件的功能,相关实现本文不再赘述。

也谈界面设计 ---- getopt 的使用

在日常的测试过程中,我们并不是每次都要迁出代码,编译代码,运行测试用例和收集测试结果。这样就需要我们能够有选择的运行部分程序功能,例如只运行测试用例和收集结果。这里我们提供了 4 个运行选泽:

选项 1:迁出代码-->编译版本-->运行测试用例-->收集测试结果

选项 2:更新代码-->编译版本-->运行测试用例-->收集测试结果

选项 3:编译版本-->运行测试用例-->收集测试结果

选项 4:运行测试用例-->收集测试结果

当然我们还需要提供帮助信息,以方便不熟悉该脚本实现的人员使用。python 也提供了 getopt 模块让我们轻松实现上述功能。实现代码参见清单 9

清单 9. 命令行写解析代码

try:
opts, args = getopt.getopt(sys.argv[1:], 'bchu', ['build', 'checkout', 'help', 'update'])
except getopt.error, msg:
self.usage()
sys.exit(2)

build_flag = 0 #构建选项
for o, a in opts:
if o in ('-h', '--help'):
self.usage()
sys.exit()
elif o in ('-c', '--checkout'):
print "执行操作:迁出代码-->编译版本-->运行测试用例-->收集测试结果"
build_flag = 1
break
elif o in ('-u', '--update'):
print "执行操作:更新代码-->编译版本-->运行测试用例-->收集测试结果"
build_flag = 2
break
elif o in ('-b', '--build'):
print "执行操作:编译版本-->运行测试用例-->收集测试结果"
build_flag = 3
break
else:
self.usage()
sys.exit()
if (0 == build_flag) :
if 2 <= len(sys.argv):
self.usage()
sys.exit()

raw_input('\n 按 Enter 键继续。。。(Ctrl+C 退出)\t')

if (1 == build_flag) : #迁出代码,并编译代码
self.checkout_code()
self.build_code()
elif (2 == build_flag) : #更新代码,并编译代码
self.update_code()
self.build_code()
elif (3 == build_flag) : #编译代码
self.build_code()

#运行测试用例并收集运行结果
self.set_python()
self.run_testsuite()
self.store_logs()

如果我们在运行的过程中想中断(如利用 Ctrl+C)一键回归测试进程的执行时,有时我们会发现虽然主进程已经被终止,但子进程仍在运行。我们能否在中断主进程的同时也中断子进程呢?答案当然是肯定的,我们可以用信号处理函数捕获信号(如捕获 Ctrl+C 产生的中断信号),然后在显式终止对应的子进程。这里就需要我们在创建子进程的时候,先保存子进程 ID,当然把子进程 ID 保存到初始化函数中,是个不错的选择,清单 10 是相关实现。

清单 10. 信号处理代码

# 终止子进程的运行
def handler(self, signum, frame):
if (-1 != self.subproc_id) : #subproc_id 定义在初始化函数中,用来存储当前子进程的 ID
os.killpg(self.subproc_id, signal.SIGINT)
sys.exit(-1)

这里我们需要在初始化函数中注册要捕获的信号,并且创建成员变量用来保存子进程的 ID,详细实现请参见清单 11。

基于对象的设计 ---- class 的使用

最后终于轮到 class 登场了,提到 class 我们就不能不谈构造函数(初始化函数)和析构函数。之前我们多次提到初始化函数,初始化函数允许我们定义一些变量,这些变量在整个类对象的生存周期内均有效。由于本文没有向系统申请资源,就再不定义析构函数了。

清单 11. 初始化处理代码

def __init__(self):
signal.signal(signal.SIGINT, self.handler) #注册需要捕获的信号量
self.myafs_dir = os.getenv('myafs')
self.subproc_id = -1 #子进程 ID,用来在终止主进程时也同时终止子进程
self.debug_log = 'log.txt' #存储详细运行日志的文件名
self.debug_fullname = os.getcwd() + os.sep + self.debug_log #全路径文件名(假设产生在该目录下)
self.sum_log = 'summary' #存储汇总日志的文件名
self.sum_fullname = os.getcwd() + os.sep + self.sum_log #全路径文件名(假设产生在当前目录下)
self.share_dir = self.utafs_dir + '/SharedFiles' #共享目录文件名

通常我们不需要太关注设计风格,只要 Python 脚本能完成我们的测试要求即可。对于较小的脚本,几条 Python 指令顺序执行即可。为了模块功能复用和可读性,我们通常会把功能模块封装成函数。本文将实现的所有函数都封装到一个类中,使得该脚本更加一体化。

清单 12. class 框架结构代码

class COneClickRegTest:
#设定一些经常使用的变量,如当前工作目录,日志名称、存储路径等
def __init__(self):

#设定 python 环境变量,实现参见代码文件
def set_python(self):

#更新代码,实现参见代码文件
def update_code(self):

#迁出代码,实现参见第 2 章代码
def checkout_code(self):

#编译版本,实现参见清单 1 代码
def build_code(self):

#运行测试集,实现参见代码文件
def run_testsuite(self):

#存储运行结果,实现参见清单 7 和清单 8 代码
def store_logs(self):

#信号处理,实现参见清单 10 代码
def handler(self, signum, frame):

#脚本使用说明,实现参见代码文件
def usage(self):

#命令行解析以及执行对应的功能,实现参见清单 9 代码
def main(self):

结束语

Python 语言是一个易学易用的脚本语言,笔者没有多久的 Python 开发经验,不过其他语言有的功能在 Python 中大都可以找到对应的实现,这也是笔者能够在很短的时间内完成该测试脚本的原因。因此,笔者把该语言和使用该语言完成一键回归测试介绍给大家,希望对大家有所帮助。正像笔者说的其他语言有的功能在 Python 中大都可以找到对应的实现,同样,如果大家对某一种特定的脚本语言或者开发语言特别熟悉,也完全可以采用所熟悉的语言来完成一键回归测试的工作。

   
次浏览       
相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践


LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...