unittest+requests+htmltestrunner:一次运行生成多份测试报告效果的设计你见过吗?(二)

unittest如何设计支持运行多个应用的接口自动化框架(一)文章中展示了用unittest实现支持多个应用的理想实现效果,并且给出了我在实际工作中遇到的一些在实现多系统实现方案在落地时遇到的设计缺陷情况。作为非技术实现方案提供者或许意识不到没实现支持多应用对整个接口自动化技术实现方案落地的影响,如果所在公司服务端系统应用个数不多,或者集成到接口自动化框架的服务端应用个数不多,确实体现不出太大差异。但是结合如今软件工程的发展情况来看,基本上去做接口自动化解决方案的时候,多是预期是要集成公司所有服务端应用的接口的。如果集成的系统(应用)在5-10个之间,或者超过10个的时候,无论时接入应用的系统(应用)相关的配置,分支管理这两项的维护工作,都有很大的工作量,甚至很大程度上影响到实际的落地效果,如果解决不了这些问题,那么在实际的落地进程中会遇到很大的阻碍,毕竟没有人愿意花时间去使用一个不好用的框架,甚至作为框架的用户,及测试代码一线开发人员,是拒绝使用框架去开发测试代码的。如果作为框架开发者来说,在这种情况下,确实是有苦难言,也就意味着落地基本是失败的。

针对上述情况我想从三类角色所站的角度去解释,

第一,作为测试代码开发人员,即接口自动化框架的用户。对于用户来说,我希望能够借助接口自动化框架开发出所有测试场景的全自动化测试代码,而框架本身对测试范围的定义、测试代码的执行甚至测试框架的通用功能我并不关注,我只管集成符合我负责的测试业务的全自动化测试代码,我只关注我开发的针对我所测试的业务系统的接口功能的测试代码能否正常执行,我的测试预期结果能否正常获取。我的测试结果能否正常表达给相关人员。那么对于框架提供者来说要做到框架通用功能对用户来说要做到是无感知的,用户只需集成满足业务测试的全自动化测试代码。

第二,作为测试领导或测试负责人角色,我的需求是用户通过接口自动化测试框架开发了全自动化的测试代码,能够满足我在测试环境对全量接口的功能得到测试质量上的保障,而在生产环境,我需要保障我的核心业务功能所覆盖的接口功能的质量是有保障的。满足我生产质量巡检需求。那么1、我需要自由筛选测试应用、自由筛选测试应用里的py文件、自由筛选py文件里的测试方法。

2、我所有测试的执行只基于master分支,无论是在测试环境还是在生产环境,CI时只会运行master分支的代码。这一点有必要发散讲一下,有一些框架提供者并没有开发出集成多系统(应用)的接口自动化框架,那么他们大都会通过创建多个分支或仓库的方式去支持他们所谓的"我的框架支持接入多系统(应用)",这种方式是不可取的,因为CI的分支或仓库越多,那么就意味着测试代码的开发分支合并到运行分支的管理工作越多,维护工作量越大。因为分支管理造成的测试代码运行错误的概率也会剧增,而之后定位出问题之后重新解决也会花时间,代码管理者和用户都需要配合工作。严重影响工作效率,也会给用户带来不好的体验,用户被搞心态。

第三,作为框架提供者,首先我要满足用户及测试领导的需求,即上述第一和第二点。而且在此基础上,我需要做到有新的系统(应用)接入时,对新系统(应用)的配置是微量的,对分支的维护也是微量的,不然我将耗费大量时间在新系统接入的配置及源码管理方面的框架通用功能的开发及bug修复上面,甚至会出现更改框架功能设计及代码实现设计上的工作。而这也是落地效果的一种体现。而针对上述三方角色的需求,如果在框架设计上没能做到公开,任何一方的不足对其它角色都是无感知的,最终体现在实际落地效果上时往往达不到落地效果的预期。

在上一章节中我们提到,pytest无法满足运行多个应用的设计,这样表达获取不太准确,更准确的表达是CI时无法精确且灵活的选择测试范围,对生产巡检需求的支持很有限,不够方便,而生产巡检需求是做接口自动化解决方案的一个核心需求。而结合目前devops平台的介入,灵活的满足生产巡检需求是一个必须条件。而unitettest在这方面相较于pytest的优势过于明显,所以unittest更有潜力成为用例组织工具,必将成为主流。

先展示下面实现的源码:

import os
import unittest
import common.HTMLTestRunner_Chart as HTMLTestRunner
from common.Log import MyLog as Log
from common.loader import CustomTestLoader
from common.deractors import timeit
from common.configEmail import MyEmail

from config_file.application_info import application_info


class AllTest:
    def __init__(self, application, receiversString, env, attr_value, is_filter=True):
        self.application = application
        self.receiversString = receiversString
        self.env = env
        self.isFilter = is_filter
        self.attr_value = attr_value

        self.log = Log.get_log()
        self.logger = self.log.get_logger()

        self.caseFile = os.path.join(os.path.dirname(os.path.dirname(__file__)), "test_case")
        self.caseList = []

        self.exe_files_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "exe_files")
        self.exe_files_list = []
        self.get_all_exe_file()

        self.reportfiles = os.path.join(os.path.dirname(os.path.dirname(__file__)), "report")

        # 是否发测试报告邮件开关,on:发送邮件,off:不发送邮件;默认不发送
        self.on_off = application_info[self.application]['report_service']['on_off']

    def get_all_exe_file(self):
        file_cases = application_info[self.application]["case"][self.env]
        [self.caseList.append(file_case) for file_case in file_cases]
        print(f'\n正在加载{self.application}应用的测试用例文件{self.caseList}在{self.env}环境运行,请等待......')

    # @timeit
    def set_case_suite(self):
        """
        Set case suite.
        """
        # Initialize the test suite
        test_suite = unittest.TestSuite()

        # Create a custom test loader with the specified attributes
        csl = CustomTestLoader(self.attr_value, self.isFilter)

        # Walk through the case directory to find the application's test cases
        case_path = None
        for root, dirs, _ in os.walk(self.caseFile):
            if os.path.split(root)[-1] == self.application:
                case_path = root
                break

        if case_path is None:
            return None

        # Load test cases based on the provided case list
        for case in self.caseList:
            case_name = case.split("/")[-1]
            # Use the custom test loader to discover and filter test cases
            discover = csl.discover(start_dir=case_path, pattern=case_name + '.py', top_level_dir=self.caseFile)

            # Add the discovered test cases to the test suite
            test_suite.addTests(discover)

        return test_suite

    def run(self, alias):
        """
        run test
        :return:
        """
        email_info = {"title": f'{alias}-接口测试'}

        self.resultPath = self.log.get_report_path(self.application)
        try:
            suit = self.set_case_suite()
            if suit is not None:
                self.logger.info('******** TEST START *********')
                fp = open(self.resultPath, 'wb')
                runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title=f'{email_info["title"]}报告', description=u'测试详细', verbosity=2, retry=0, save_last_try=False)
                result = runner.run(suit)
                email_info['result'] = runner.getReportAttributes(result)
                fp.close()

            else:
                self.logger.info('******** HAVE NO CASE TO TEST ********')
        except Exception as e:
            self.logger.error(str(e))
        finally:
            self.logger.info('********** TEST END *********')
        self.send_test_report_by_email(email_info)

    def send_test_report_by_email(self, email_info):
        # 生成邮件
        self.email = MyEmail(self.application).get_email(email_info)
        # send test report by email
        if self.on_off == 'on':
            self.email.send_email(self.receiversString)
        elif self.on_off == 'off':
            self.logger.info("Doesn't send report email to developer.")
        else:
            self.logger.info("Unknow state.")

针对上述结论,我们举个具体例子对比说明,我们举生产巡检需求的例子:

某次生产巡检中,我需要运行两个系统中部分测试文件中的部分测试方法,更具体一点,example1,example2两个应用,e1和e2下有全量测试代码py文件分别有两个,每个py文件中全量测试方法有5个,

生产巡检场景1:运行e1的sc.py中全量(或者mark属性值为smoke,a)的测试方法+e2的scc.py的全量(或者mark属性值为smoke,a)的测试方法

生产巡检场景2:运行e1的sc_e1.py中全量的测试方法+e2的sccc.py的全量的测试方法

下面演示效果,测试代码的源码之前章节已有,不重复贴出,就是5个测试方法:

场景1:运行e1的sc.py中全量(或者mark属性值为smoke,a)的测试方法+e2的scc.py的全量(或者mark属性值为smoke,a)的测试方法

场景2:运行e1的sc_e1.py中全量的测试方法+e2的sccc.py的全量的测试方法

上面表达了unittest相较于pytest对于满足生产巡检需求的优势过于明显的观点,其实这个的本质原因是由于unittest和pytest加载用例的机制决定的。unittest加载用例的机制是基于测试类,unittest.TestCase,去加载测试类下的test开头的方法(其实这个属性可以自定义,可以修改为非test开头的方法);而pytest加载测试用例的机制是没有测试类的概念的,它只是去加载test开头的方法(同样可以修改前缀)。这就决定了一个特性:unittest的应用、测试代码py文件、测试代码py文件的测试方法都可以自由筛选,而pytest则是应用、应用测试代码py文件,py文件下的测试方法都无法做到自由筛选,如果要指定必须通过配置的方式去事先定义好,然而多项目的情形下,假设一个应用有5个生产巡检需求的组合,一共10个应用,那么命令上就是5✖10,那么维护这50个命令也是不少的工作量,而且生成了这50个命令 ,在CI的体现上,Jenkins就是50个job,devops对应就是50个任务。而unittest的实现却只有1个job及1个devops任务,这个优势明显程度无需多言,如果框架是针对单个应用部署一套框架的玩家,请忽略。

下面演示unittest实现应用、py文件、测试方法自由选择的效果:

1、应用自由筛选

2、测试代码py文件自由筛选

3、测试代码py文件的测试方法自由筛选:

总结:基于pytest加载用例的机制,有些开发人员在用pytest开发测试框架时,甚至都不会考虑支持多个应用的情况,也就没有1次执行多份测试报告发送给各自应用研发团队效果的设计。

另外,我撰写基于unitest实现接口自动化框架专题的初衷首先也是希望给想了解或攻克这项技术的同学一个学习参考的方向,更多的是偏向设计思路,以便再之后开发新的项目时也会有所提升。我即不建议jmeter也不建议pytest,其实pytest号称对unittest的功能进行升级,然而实际上升级的功能并没有什么卵用。另外也在此对unittest发声,也希望大家放过unittest,不要给unittest打上过时或者不如pytest的标签。其实pytest也是基于unitest进行开发的,从研究的层面上来说,unittest的拓展性和可玩性肯定是更高的,开发自己的第一套接口自动化框架,unittest是首选。

再次,我也希望读者能踊跃的指出我的错误或设计不足,我是一个有技术追求的同学,乐于分享,技术就是拿来分享的,欢迎大家多多留言讨论这项技术的实现和设计,或者是读者刚好也在实现自己的框架时的需求,一起为技术的探索贡献自己的一份力量吧。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值