使用 Python 在 Linux 上实现一键回归测试

新媒体管家

点击上方“Python开发”,选择“置顶公众号”

关键时刻,第一时间送达!

使用 Python 在 Linux 上实现一键回归测试

使用 Python 在 Linux 上实现一键回归测试

从代码库迁出代码 —- 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(" 按 Enter 键继续。。。(Ctrl+C 退出)")

        

    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 中大都可以找到对应的实现,同样,如果大家对某一种特定的脚本语言或者开发语言特别熟悉,也完全可以采用所熟悉的语言来完成一键回归测试的工作。使用 Python 在 Linux 上实现一键回归测试

  • 来自:IBMdeveloperWorks

  • www.ibm.com/developerworks/cn/linux/1405_dingcj_pythontest/index.html

  • Python开发整理发布,转载请联系作者获得授权

  • 使用 Python 在 Linux 上实现一键回归测试

    使用 Python 在 Linux 上实现一键回归测试
    【点击看Java编程精选】