全面实现HttpRunner并局部优化(二)增加UI测试

基于全面实现HttpRunner并局部优化(一)继续优化:

  • API testcase层面的设置参数化执行次数(优化)
  • 基于selenium 加入UI 测试,并优化UI测试报告加入错误截图以及测试类级别的依赖跳过机制。
  • 执行方式采用 CLI 调用,可与 Jenkins 等持续集成工具完美结合
  • 一些细节优化(线程级别的清理,时间统计,添加测试方法,指定线程数,yaml模板格式等)

关于API

在第一版使用过程中遇到以下场景,并根据实际情况做出以下优化:

场景1:

第一步:登录

第二步:创建一个店铺

第三步:创建100个商品

按照之前的逻辑,若在第三步设置执行次数,将会在原基础上添加第三步的方法。例如cycles = 2 , 则会生成两个测试类,都会以第一步,第二步的方法为基础上,添加第三步方法。 对于一些登录次数短时间内有限制且为第三方控制的系统上,这样做增大了账号禁止登陆的风险。 虽然可以通过定义setupClass控制前置条件,但这么做还是不怎么优雅~

cycles = teststep["teststep"]['cycles']
testcases = [eval(testcase)[:index+1][:] for _ in range(cycles+1)]
for index_,testcase_ in enumerate(testcases[1:]):
     testcase_.append(eval(testcase)[index+1])   # 复制在此之前的step
     testcase_[0]['config']['description'] += f'_{str(index_)}'  # 重新定义_doc_
     testcase_[0]['config']['name'] += f'_{str(index_)}'     # 重新定义class
     class_name = cls.create_class(str(testcase_))

由此背景下,添加关键字loop,若为True,则在原基础上添加方法。改为所有测试方法的的所属类都为一个,效果如下:

for _ in range(int(teststep["teststep"]['cycles'])):
    def_name = teststep["teststep"]['name'][:]
    teststep["teststep"]['name'] += f'_{_}'
    func = cls.create_def(teststep, class_name, index, config)
    suite.addTest(class_name(func))
    teststep["teststep"]['name'] = def_name

场景2:需要登陆用户创建指定的商品。

在此之前创建的商品 均来自函数助手生成的随机数据,无法指定商品的属性,由此模仿httprunner参数化机制,添加Parameterizes关键字,以变量-变量的方式当作key,文件名为value.

- teststep:
    name: DepenceThr
    description: 测试接口A3==>获取变量Thr
    request:
        headers:
            User-Agent: ${ua_random()}
        contentType:
        data:
          user:
            id:
        method: get
        extract:
            - Thr
        url: http://localhost:9998/DepenceThr
    check:
      assertEqual:
        status: 200
      assertIn:
        content:
          - Thr
    cycles:  3 
    loop: True
    Parameterizes:
      user-id: test.csv

主要的代码在处理 loop(在当前测试类下生成测试方法),cycles(循环次数),Parameterizes(参数化读取)之间的关系,最终逻辑为在有cycles 的情况下,以cycles为循环次数,遍历Parameterizes。若cycles>Parameterizes.lines 则循环Parameterizes文件。

关于UI

大部分人提到UI 想到的是PO模式,但本着框架不写代码的信仰并结合yaml文件格式限制..故此次采用关键字驱动的方式来执行测试,由于httprunner 没有UI 测试的机制,导致UI yaml文件约定格式比较难制定(没有地方可以抄了)….所有有更好方案的可以一起讨论呀~

PO是Page Object的缩写,PO模式是自动化测试项目开发实践的最佳设计模式之一。
核心思想是通过对界面元素的封装减少冗余代码,同时在后期维护中,若元素定位发生变化, 只需要调整页面元素封装的代码,提高测试用例的可维护性、可读性。
PO模式可以把一个页面分为三层,对象库层、操作层、业务层。
对象库层:封装定位元素的方法。
操作层:封装对元素的操作。
业务层:将一个或多个操作组合起来完成一个业务功能。比如登录:需要输入帐号、密码、点击登录三个操作。

基本模板(testcase):每个info代表一个函数(测试函数),operate_type则为制定的关键字。

- config:
    name: Baidu_Demo
    description: 百度页面测试
    variables:
    driver:
      over_time: 5
      time_span: 1
      driver_name: chrome
    hooks:
      setUpClass:
      tearDownClass:

- teststep:
  - info: open_url
    description: 打开百度首页
    path: /
    operate_type: start

  - info: click_search
    description: 点击搜索按钮
    element: //*[@id="su"
    find_type: xpath
    operate_type: click

  - info: get_title
    description: 获取页面标题
    operate_type: title
    check:

  - info: quit
    description: 关闭浏览器
    operate_type: quit

项目文件路径如下,project下为项目名,每个项目均有一个配置文件config.ini,用于不同环境下的配置参数以及账号信息。api ,ui 则分别放入其测试文件,相比api测试目录,ui 将其分为cases,以及suites目录,使用场景有:

第一步:注册用户A(商品创建员)

第二步:注册用户B(商品审核员)

第三步:  用户A 创建物品甲

第四步:用户B审核物品甲

第五步:普通用户购买物品甲

cases文件下,将每一步 ,写成一个yaml文件,方便单个步骤调试。

suites文件下,将每一个cases 通过yaml 引用的方式,生成特定的测试场景。主要解决多线程下保持suites 级别独立,cases级别依赖。

+--project
| +--Test
| | +--api
| | | +--testcase_logic_xx1.yaml
| | +--config.ini
| | +--ui
| | | +--cases
| | | | +--test_baidu.yaml
| | | | +--test_youdao.yaml
| | | +--suites
| | | | +--test_suite.yaml

封装selenium(仅部分关键字&部分代码),此处代码注释掉了基于__new__实现的单例模式,原因是多线程执行时Driver会混乱~init_driver方法用来简单判断参数并返回实例化后的Driver,keywords 则为关键字 字典,每个关键字对应着一个lambda 表达式,相较与多层多层elif 更加清晰~

class Driver:

    # def __new__(cls, *args, **kwargs):
    #     if not hasattr(Driver, "_single"):
    #         Driver._instance = object.__new__(cls)
    #     return Driver._single

    def __init__(self,over_time=5,time_span=1,driver_name="Chrome",options=None):
        try:
            self.over_time = over_time
            self.timespan = time_span
            self.keywords = {
                'start':lambda  url: self.start(url),
                'click': lambda element: self.find_element_click(element),
                'input':lambda element,msg: self.find_element_input(element,msg),
                'screenshot':lambda : self.screenshot(),
                'sleep':lambda s=12:self.sleep(s),
                'refresh':lambda :self.refresh(),
                'title':lambda :self.get_title(),
                'url':lambda :self.get_url(),
                'text':lambda element:self.get_text(element),
                'get_attribute':lambda element,attribute:self.get_attribute(element,attribute),
                'quit':lambda :self.quit(),
            }
            if driver_name.lower() == "firefox":
                self.driver = webdriver.Firefox(executable_path=firefox_driver)
            elif driver_name.lower() == "ie":
                self.driver = webdriver.Ie(ie_driver)
            elif  driver_name.lower() == "chrome":
                self.driver = webdriver.Chrome(chrome_driver, options=options)
            else:
                raise Exception(f'{driver_name}未找到该驱动')
            log.success(str(driver_name)+'==>初始化成功!')
        except Exception:
            log.error(traceback.format_exc())
            raise

    @classmethod
    def init_driver(cls,over_time,time_span,driver_name):
        option = webdriver.ChromeOptions()
        option.add_argument('--headless')
        option.add_argument('--disable-gpu')
        option.add_argument("--window-size=1920,1080")  #
        option.add_argument("--hide-scrollbars")
        option.add_experimental_option('excludeSwitches', ['enable-logging','enable-automation'])
        if isinstance(over_time,int) & isinstance(time_span,int):
            return cls(over_time,time_span,driver_name,options=option)
        else:
            raise AttributeError('over_time与time_span参数必须为整数')

执行测试代码时对于多线程的实例Driver的判定,若不存在Driver_id 则立刻初始化一个实例,若存在Driver_id,但依赖case name 为空则也重新初始化一个实例(此前设计为一个yaml对应一个线程,多指定线程数量小于yaml文件的数量,则会导致实例被复用,但此实例又可能已被关闭),在except下Variable = NoReprotVariable,解决CLI不生成测试报告时运行机制不被打断的问题。

try:
    Variable = getattr(Variables, str(threading.currentThread().getName()))
    Driver_id = f'Driver{threading.currentThread().getName()}'  # 获取当前线程driver id
    driver_config = config['config'].get('driver',
                                         {'over_time': 5, 'time_span': 1, 'driver_name': 'Chrome'})
    if not hasattr(Variable, Driver_id):
        driver = Driver.init_driver(**driver_config)  # 如果不存在则立即初始化
        log.success(f'创建suite Driver成功!==>:{driver}')
        log.success('>>>>>>>>Driver类型:%(driver_name)ss 最长等待时间:%(over_time)ss 查找间隔时间:%(time_span)ss' % driver_config)
        setattr(Variable, Driver_id, driver)
    elif not dependon_name:
        driver = Driver.init_driver(**driver_config)  # 如果没有依赖case 则判定为新suite
        log.success(f'重新创建suite Driver成功!==>:{driver}')
        log.success('>>>>>>>>Driver类型:%(driver_name)ss 最长等待时间:%(over_time)ss 查找间隔时间:%(time_span)ss' % driver_config)
        setattr(Variable, Driver_id, driver)
    else:
        driver = getattr(Variable, Driver_id)

except Exception:
    try:
        Variable = NoReprotVariable
        if not hasattr(Variable, 'Driver'):
            driver_config = config['config'].get('driver',
                                                 {'over_time': 5, 'time_span': 1, 'driver_name': 'Chrome'})
            driver = Driver.init_driver(**driver_config)
            log.success(f'创建suite Driver成功!==>:{driver}')
            log.success('>>>>>>>>Driver类型:%(driver_name)ss 最长等待时间:%(over_time)ss 查找间隔时间:%(time_span)ss' % driver_config)
            setattr(Variable, 'Driver', driver)
        driver = getattr(Variable, 'Driver')
    except Exception:
        log.error(traceback.format_exc())
        raise

 

suite级别模板,因分层级别较多(project级,testsuite级,testcase级)故可在不同级添加 setUp,tearDown实现类似pytest下的fixture scope

- config:
    name: Baidu_Demo
    description: 多case联动
    variables:
    driver:
      over_time: 5
      time_span: 1
      driver_name: chrome
    hooks:
      setUpClass:
      tearDownClass:

- testcase:

  - case: Test/ui/cases/test_baidu.yaml
    description: 百度页面简单测试

  - case: Test/ui/cases/test_youdao.yaml
    description: 有道页面简单测试

 

关于测试报告,因ui更加贴切的模拟了用户的点击,输入等操作,相对api ,其失败截图显得相当的重要。通过检索前辈对于ui报告截图的实践,类似都是在报告中添加截图的路径(尤其是本地路径),对于邮件中的报告友好度较差,没有公网ip则无法访问,有公网ip也要做相关的存储工作,最终采用base64的方式将图片存入报告中。

约定一个输出格式:

print(f'selenium:{time_format}')

捕获输出并判断:

if 'selenium' in uo:
 image_photo = selenium_image + f'\selenium{uo.strip()[9:]}.png'
 script = self.REPORT_TEST_OUTPUT_TMPL % dict(
 output=saxutils.escape(uo + ue),
 )

写入模板:

with open(image_photo, 'rb') as f:
    row = tmpl % dict(
        tid=tid,
        Class='',
        style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or (n == 3 and 'skipCase' or 'passCase'))),
        desc=desc,
        script=script,
        status=self.STATUS[n],
        times=times,
        image_base64=base64.b64encode(f.read()).decode('utf-8')
    )

结果:

关于CLI

相对于httprunner 使用内置库argparse 来实现,此次用Click 第三方库来实现,使用起来更加的简单与强大~

使用@click.group() 集成ui,api命令,部分代码如下

@click.group()
def run():
    pass

@click.command()
@click.option('--log', default = None,help='开启日志(参数随便填,有值则开启。)',)
@click.option('--file', help='输入脚本文件yaml')
@click.option('--path', help='输入脚本文件yaml所在文件夹')
@click.option('--report-email',default = 'no', help='yes为输出报告不发邮箱/no为输出报告并发送邮箱',type=click.Choice(['yes', 'no','False']))
@click.option('--threads', default = None,help='线程数(int)最大线程数为500 默认为yaml文件的个数',type=click.IntRange(1, 500))
@click.option('--config', default = None,help='若有project层的变量时,需要引入config文件路径',type=str)
@click.option('--env', default = 'ci',help='环境默认为ci',type=str)
def ui(report_email,log,env,threads=None,path=None,file=None,config=None):
    if  not path and not file:
        click.echo('必须填写path or file!! 输入"run ui --help"查看详情~' )
        exit(0)
    global LOG_FLAG
    if not log: LOG_FLAG = False
    if report_email.lower() == 'false':
        report_email = False
    __run_with_ui(file, path, threads,env,config,report_email)

@click.command()
@click.option('--log', default = None,help='开启日志(参数随便填,有值则开启。)',)
@click.option('--file', help='输入脚本文件yaml')
@click.option('--path', help='输入脚本文件yaml所在文件夹')
@click.option('--report-email',default = 'no', help='yes为输出报告不发邮箱/no为输出报告并发送邮箱',type=click.Choice(['yes', 'no','False']))
@click.option('--threads', default = None,help='线程数(int)最大线程数为500 默认为yaml文件的个数',type=click.IntRange(1, 500)) #click.Choice([str(i) for i in range(501)])
def api(report_email,log,threads=None,path=None,file=None):
    if  not path and not file:
        click.echo('必须填写path or file!! 输入"run api --help"查看详情~' )
        exit(0)
    global LOG_FLAG
    if not log: LOG_FLAG = False
    if report_email.lower() == 'false':
        report_email = False
    __run_with_api(file, path,threads,report_email)

run.add_command(ui)
run.add_command(api)
C:\Users\c4185>run --help
Usage: run [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  api
  ui
C:\Users\c4185>run ui  --help
Usage: run ui [OPTIONS]

Options:
  --log TEXT                     开启日志(参数随便填,有值则开启。)
  --file TEXT                    输入脚本文件yaml
  --path TEXT                    输入脚本文件yaml所在文件夹
  --report-email [yes|no|False]  yes为输出报告不发邮箱/no为输出报告并发送邮箱
  --threads INTEGER RANGE        线程数(int)最大线程数为500 默认为yaml文件的个数
  --config TEXT                  若有project层的变量时,需要引入config文件路径
  --env TEXT                     环境默认为ci
  --help                         Show this message and exit.

TODO: CLI实现postman最新版本用例导出(API)

TODO: 集成locust实现压力测试,并完善对应测试报表(性能)

TODO: 解析selenium IDE 脚本内容映射yaml (UI)

新年快乐哟~ 

269,105 次浏览

“全面实现HttpRunner并局部优化(二)增加UI测试”的11,395个回复


    Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20623360 bytes) in /www/wwwroot/ayoc.top/wp-includes/comment-template.php on line 2101