python3+uiautomator2+pytest+allure-demo+POM模型实现APP自动测试框架

python3+uiautomator2+pytest+allure-demo+POM模型实现APP自动测试框架【裙子:822659419】

执行视频https://live.csdn.net/v/216708

源码见https://blog.csdn.net/qq_42846555/article/details/126173704

一. 实现的功能

  1. 使用POM模型,分层思想,封装基本base_pege类(包含点击,输入,打开关闭APP,截图,录屏,断言,滑动等),业务逻辑类(业务,断言,页面元素),测试用例类,测试数据类
  2. 使用pytets测试框架组装测试,具有,失败自动重跑,多重断言,以及筛选标签或用例等级测试,等等具有pytets测试框架的特性
  3. 使用allure报告,汇总测试结果,展示精美的测试报告
  4. 多进程实现的多设备同时执行用例,各个设备测试数据独立(各设备登录不同账户),进行用例压力测试
  5. 实现多设备之间交互业务,比如一台设备拨打电话,另一台进行接听电话
  6. 将用例操作录屏附在allure测试报告

二. 项目结构

├─app
│      base.apk               #测试APP
│      
├─base
│  │  base_page.py           #封装基本base_pege类
│  │  __init__.py
│          
├─business
│  │  __init__.py
│  │  
│  ├─tiku
│  │  │  tiku_assert.py     #tiku,模块断言
│  │  │  tiku_bs.py         #tiku,模块业务逻辑
│  │  │  tiku_page.py       #tiku,模块业务基本元素
│  │  │  __init__.py
│  │  │  
│  │          
│          
├─common
│  │  all_path.py           #路径设置
│  │  app_info.py           #获取APP属性信息包名,等等
│  │  get_devices.py        #adb 获取设备列表,等 adb 实现的其他功能
│  │  logger.py             #打印日志
│  │  readCofig.py          #读取 Cofig数据
│  │  read_yaml.py          #读取 yaml数据
│  │  __init__.py
│  │  
│          
├─config
│  ├─pre
│  │      data.yml          #测试数据
│  │      
│  └─test
│          data.yaml         #测试数据
│          devices_info_multiple.yaml    #多设备交互类用例执行配置文件
│          devices_info_one.yaml         #单设备执行用例执行配置文件
│          group_data_1.yaml             #设备1用例测试数据
│          group_data_2.yaml             #设备2用例测试数据
│          
├─test_case                 #交互型测试用例
│      test_file_01.py
│      __init__.py
│      
├─test_case_mtbf              #非交互型测试用例
│  │  test_001.py
│  │  __init__.py
│
├─log                       #日志保存目录
├─png                       #测试截图保存目录
├─screencap                 #测试录屏保存目录
│
│  conftest.py              #pytest框架的conftest文件
│  pytest.ini               #pytets框架的标签配置文件
│  requirements.txt         #项目依赖
│  run.py                   #运行入口

三. 部分代码

#!/user/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# @File : run.py
# @Time : 2022-04-01 15:05
# @Author : mojin
# @Email : 397135766@qq.com
# @Software : PyCharm
#-------------------------------------------------------------------------------
import argparse
parser = argparse.ArgumentParser(description='manual to this script')
parser.add_argument('--string', type=str, default=None)
args = parser.parse_args()
#python3 run.py --string=devices_info.yaml
#接收命令行参数
#args.string
#https://blog.csdn.net/guohewei123/article/details/88602524
import shutil
import pytest,time
import subprocess
from multiprocessing import Pool
from common.get_devices import *
from common.logger import Logger

def run_parallel(devices_info):
    # 编写规则
    # 编写pytest测试样例非常简单,只需要按照下面的规则:
    # 测试文件以test_开头(以_test结尾也可以)
    # 测试类必须以Test开头,并且不能带有init方法
    # 测试函数以test_开头
    # 断言使用基本的assert即可
    # pipreqs . --encoding=utf8 --force #导出所需依赖
    # pip3 install -r requirements.txt -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
    # pip3 install -r requirements.txt -i http://pypi.doubanio.com/simple/ --trusted-host pypi.doubanio.com
    # pip install allure-pytest #报错ImportError: cannot import name 'get_testplan'
    # pip3 install pytest-rerunfailures
    # pip3 install pytest-repeat #失败重跑
    #pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
    #pytest.main()不带任何参数的main()运行当前目录下所有(test_*.py  和 *_test.py)
    # pytest.main():main中传入不同的指令用以执行指定测试用例
    # -s: 显示程序中的print/logging输出
    # -v: 丰富信息模式, 输出更详细的用例执行信息
    # -q: 安静模式, 不输出环境信息
    # -k:关键字匹配,用and区分:匹配范围(文件名、类名、函数名)   pytest.main (['-k','pp']) #'-k',' test_answer',
    # (1)‘-s’:关闭捕捉,输出打印信息。
    # (2)‘-v’:用于增长测试用例的冗长。
    # (3)‘-k’ :运行包含某个字符串的测试用例。如:pytest - k add XX.py 表示运行XX.py中包含add的测试用例。
    # (4)‘q’:减小测试的运行冗长。
    # (5)‘-x’:出现一条测试用例失败就退出测试。在调试阶段很是有用,当测试用例失败时,应该先调试经过,而不是继续执行测试用例。函数基础示例
    # '-m finished','./test_case','--alluredir', './report/allure',     @pytest.mark.finished #用例标记 pytest -m "finished and commit" 测试时pytest -m "finished and commit"
    # —allure-severities=blocker,critical用例严重级别执行
    # reruns = 5:意思是失败重运行5次
    # count = 3:意思是重复执行3次
    #  --lf 叫--last-failed如果只想运行其中2个failed的和1个error用例,那么可以运行pytest - -lf,这样就只运行非passed的用例
    #  --ff 叫--failed-first如果想先运行上次失败的,后运行其他通过的用例,那么可以运行pytest - -ff
    runtime = time.strftime("%Y%m%d%H%M%S", time.localtime())
    # xml_addr='./test_report/%s_report.xml'%runtime
    # html_addr='./test_report/%s_report.html'%runtime
    xml_addr = './target/test_report/report.xml'
    html_addr='./target/test_report/report.html'
    Logger.info(devices_info)
    #pytest.main([f"--cmdopt={devices_list}",'./test_case_mtbf','-vs','--alluredir', './target/allure-results','--html=%s'%html_addr,'--junitxml=%s'%xml_addr,'--self-contained-html'])
    #pytest.main([f"--cmdopt={devices_info}", './test_case','-vs',"--reruns=1",'--ff',"--alluredir", "target/allure-results"])
    pytest.main([f"--cmdopt={devices_info}", '%s'%devices_info['test_case_path'],"--reruns=1", '-vs', "--alluredir","target/allure-results"])#"--reruns=1",'--count=10',
    allure_html = 'allure generate ./target/allure-results -o ./target/allure-report --clean'  # 生成allure的html报告
    subprocess.call(allure_html, shell=True)  # 生成allure的html报告
    # allure_cmd = "allure serve ./target/allure-results"
    # subprocess.call(allure_cmd, shell=True)  # 启动allure报告
    
def run(Always='False'):
    # Always = "True"长时间循环运行运行;
    # 默认Always = "False" 运行一次;
    # Always = "5", 运行5次;
    # 【注意给带引号,"True","False","5"字符串类型】;
    try:
        shutil.rmtree("./target")  # 删除报告目录,删除后只能看到当前执行后的报告结果,不删除能看到执行的历史执行结果
    except:
        pass
    yaml_info_lsit=["devices_info_one.yaml"]#devices_info_one非交互型用例配置文件,devices_info_multiple.yaml交互型用例配置文件
    #yaml_info_lsit=["devices_info_multiple.yaml"]#devices_info_multiple交互型用例配置文件
    for yaml_name in  yaml_info_lsit:
        devices_info=read_yaml_devices_info_list_one(yaml_name)
        if Always == "True":  # Always==True长时间循环运行
            n = 1
            while True:
                Logger.info('<%s开始运行第“%s”轮测试%s>' % ("=" * 40,n, "=" * 40))
                with Pool(len(devices_info)) as pool:
                    pool.map(run_parallel, (devices_info))
                    pool.close()
                    pool.join()
                Logger.info('<%s运行第“%s”轮测试结束%s>' % ("=" * 40,n, "=" * 40))
                n += 1
        elif Always == "False":  # Always==False运行一次
            Logger.info('<%s开始运行测试%s>' % ("=" * 40, "=" * 40))
            with Pool(len(devices_info)) as pool:
                pool.map(run_parallel, (devices_info))
                pool.close()
                pool.join()
            Logger.info('<%s运行测试结束%s>' % ("=" * 40,  "=" * 40))
        elif str.isdigit(str(Always)):#判断输入的值是否为数字
            n = 1
            while n<=int(Always):
                Logger.info('<%s开始运行第“%s”轮测试%s>' % ("=" * 40,n, "=" * 40))
                with Pool(len(devices_info)) as pool:
                    pool.map(run_parallel, (devices_info))
                    pool.close()
                    pool.join()
                Logger.info('<%s运行第“%s”轮测试结束%s>' % ("=" * 40,n, "=" * 40))
                n += 1
        else:
            Logger.info('“Always”输入的值不符合要求,Always值可以为:True or False or 数字')
            # allure_cmd = "allure serve ./target/allure-results"
            # subprocess.call(allure_cmd, shell=True)  # 启动allure报告
            
if __name__ == '__main__':
    # read_yaml_devices_info_list()读取配置文件设备信息
    # #devices_info_list()自动获取设备列表组成项目需要的列表形式(两台设备一组,一组就是一个进程)
    #python3 run.py --string=devices_info.yaml
    # Logger.info('读取设备配置文件名为:%s'%args.string)
    # devices_info = read_yaml_devices_info_list(args.string)#命令栏获取配置文件名称进行获取设备列表
    #devices_info = read_yaml_devices_info_list_one(read_yaml_name)#获取设备列表
    # Always="True"长时间循环运行运行;默认Always="False"运行一次;Always="5",运行5次【注意给带引号,"True","False","5"字符串类型】
    run(Always="False")

#!/user/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# @File : conftest.py
# @Time : 2022-04-01 14:42
# @Author : mojin
# @Email : 397135766@qq.com
# @Software : PyCharm
#-------------------------------------------------------------------------------
from common.read_yaml import ReadYaml
from base.base_page import BasePage
import random
import pytest,time,allure,os
from common.app_info import get_app_name_nopath
from common.logger import Logger
import uiautomator2 as u2


#命令行传参addoption 在contetest.py添加命令行选项,命令行传入参数”—cmdopt“, 用例如果需要用到从命令行传入的参数,就调用cmdopt函数:
def pytest_addoption(parser):
    parser.addoption("--cmdopt", action="store", default="device_info", help=None)

@pytest.fixture(scope='session')
def cmdopt(pytestconfig):
    # 两种写法
    #global parameter_data
    parameter_data=eval(pytestconfig.getoption("--cmdopt"))
    return parameter_data
    # return pytestconfig.option.cmdopt

@pytest.fixture(scope='session')
def group_data(cmdopt):#读取数据源文件
    yaml_name =(cmdopt)['group_data']  # 获取yaml文件名称
    data = ReadYaml(yaml_name).get_yaml_data()  # 获取到该组设备对应的yaml文件数据
    return data

# @pytest.fixture(params=group_data)
# def group_data_params(request):
#     """用例数据,测试方法参数入参该方法名 cases即可,实现同样的参数化
#     目前来看相较于@pytest.mark.parametrize 更简洁。
#     """
#     return request.param

# fixture传参
# scope参数有四种,分别是'function','module','class','session',默认为function。
# function:每个test都运行,默认是function的scope
# class:每个class的所有test只运行一次
# module:每个module的所有test只运行一次
# session:每个session只运行一次

@pytest.fixture(scope="session")
def drivers(cmdopt):
    global dri,dev
    dev = (cmdopt)['devices_list']
    dri=[]
    for i in dev:
        d=u2.connect(i)
        d.app_install(get_app_name_nopath())#安装应用
        Logger.info(d)
        dri.append(d)
    return dri,dev

# @pytest.fixture(scope="function")
# def open_app():
#     data_Yaml_package = data_Yaml['package']
#     Logger.info('<%s开始%s>'%("="*20, "="*20))
#     i = 0
#     for k, v in data_Yaml_package.items():
#         dri[i].screen_on()
#         #dri[i].unlock()  # 解锁设备
#         dri[i].set_fastinput_ime(True)  # 启用自动化定制的输入法
#         dri[i].implicitly_wait(20)  # 设置隐式等待时间,默认20s
#         dri[i].watcher.start()
#         dri[i].watcher.when("允许").click()
#         dri[i].watcher.when("立即开始").click()
#         dri[i].watcher.when("仅在使用中允许").click()
#         dri[i].watcher.when("确定").click()
#         dri[i].watcher.when("取消").click()
#
#         dri[i].watcher.when("以上都不是").click()
#         Logger.info("设备“%s”,打开“%s”APP" % (dev[i], v))
#         dri[i].app_start(v,stop=True)# 指定包名启动APP,启动前先结束应用运行状态
#         i += 1
#
#     yield
#
#     Logger.info('执行完成,10秒后关闭APP')
#     time.sleep(10)
#
#     #for i in range(len(data_Yaml_package)):
#     i=0
#     for k, v in data_Yaml_package.items():
#         dri[i].app_stop(v)
#         Logger.info("设备“%s”,关闭“%s”APP" % (dev[i], v))
#         dri[i].set_fastinput_ime(False)  # 关闭自动化定制的输入法
#         dri[i].watcher.reset()
#         #d.screen_off()
#         i += 1
#
#     Logger.info('<%s结束%s>' %("="*20, "="*20))

#@pytest.fixture(scope="function", autouse=True)#scope参数为默认的function:每个test都运行一次,autouse=True用例函数不用输入调用,默认自动执行
@pytest.fixture(scope="function")#,autouse=True
def open_iphone():
    from base.base_page import BasePage
    Logger.info('<%s开始%s>'%("="*20, "="*20))
    n=0
    runtime = time.strftime("%Y%m%d%H%M%S", time.localtime())
    screenrecord_name = '%s_' % runtime
    screenrecord_name_lsit=[]
    for i in dev:
        BasePage=BasePage(dri[n])
        dri[n].screen_on()
        dri[n].set_fastinput_ime(True)  # 启用自动化定制的输入法
        dri[n].implicitly_wait(20)  # 设置隐式等待时间,默认20s
        dri[n].watcher.start()
        dri[n].watcher.when("允许").click()
        dri[n].watcher.when("立即开始").click()
        dri[n].watcher.when("仅在使用中允许").click()
        dri[n].watcher.when("确定").click()
        dri[n].watcher.when("取消").click()
        dri[n].watcher.when("以上都不是").click()
        Logger.info('“%s”手机唤醒' % i)
        #dri[n].screenrecord(('%s%s')%(screenrecord_name,i))
        BasePage.screenrecord_start(('%s%s')%(screenrecord_name,i),doc='开始录屏……')
        screenrecord_name_lsit.append(('%s%s')%(screenrecord_name,i))
        n+=1
    yield [screenrecord_name_lsit]
    Logger.info('执行完成,10秒后关闭APP')
    time.sleep(3)
    n = 0
    for i in dev:
        #dri[n].screen_off()
        #dri[n].screenrecord.stop()
        dri[n].set_fastinput_ime(False)  # 关闭自动化定制的输入法
        dri[n].watcher.reset()
        #Logger.info('“%s”手机息屏'%i)
        #d.screen_off()
        #BasePage.screenrecord_sotp( '%s%s'%(screenrecord_name,i),doc='结束录屏……')
        n += 1
    Logger.info('<%s结束%s>' %("="*20, "="*20))


# setup和teardown操作
# setup,在测试函数或类之前执行,完成准备工作,例如数据库链接、测试数据、打开文件等
# teardown,在测试函数或类之后执行,完成收尾工作,例如断开数据库链接、回收内存资源等
# 备注:也可以通过在fixture函数中通过yield实现setup和teardown功能


def pytest_itemcollected(item):
    # item._nodeid = '%s_%s_%s' %(dev[0],dev[1],item . _nodeid)
    #runtime = time.strftime("%Y%m%d%H%M%S", time.localtime())
    item._nodeid = str(random.randint(1, 1000)) + '_' + item._nodeid
    #item._nodeid = str(runtime) + '_' + item._nodeid


#!/user/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# @File : tiku_page.py
# @Time : 2022-06-01 16:59
# @Author : mojin
# @Email : 397135766@qq.com
# @Software : PyCharm
#-------------------------------------------------------------------------------


from common.read_yaml import ReadYaml
data_Yaml = ReadYaml("data.yaml").get_yaml_data()  # 读取数据
#data_Yaml_package = data_Yaml['package']
class TiKu_Page():

    tv_agree = ("resourceId", "com.gsx.tiku:id/tv_agree")  # 同意使用并开始
    ShanYanOneKey=("text","其他方式登录")

    tv_login_pwd=("resourceId","com.gsx.tiku:id/tv_login_pwd")#切换为密码登录
    LoginID=('XPath','//*[@resource-id="com.gsx.tiku:id/layoutContent"]/android.widget.RelativeLayout[1]')#输账号框
    LoginPWD=('XPath','//*[@resource-id="com.gsx.tiku:id/layoutContent"]/android.widget.RelativeLayout[2]')#输入密码框
    iv_select=("resourceId","com.gsx.tiku:id/iv_select")#勾选用户协议
    bt_login = ("resourceId", "com.gsx.tiku:id/bt_login")  # 登录按钮
    tvName= ("XPath", '//*[@resource-id="com.gsx.tiku:id/recyclerView"]/android.widget.FrameLayout[5]/android.widget.LinearLayout[1]')  # 我的
    #d.xpath('//*[@resource-id="com.gsx.tiku:id/recyclerView"]/android.widget.FrameLayout[5]/android.widget.LinearLayout[1]')
    tvNickName=("resourceId", "com.gsx.tiku:id/tvNickName")  # 账户id






#!/user/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# @File : tiku_bs.py
# @Time : 2022-06-01 16:22
# @Author : mojin
# @Email : 397135766@qq.com
# @Software : PyCharm
#-------------------------------------------------------------------------------

from base.base_page import BasePage
import uiautomator2 as u2
from common.logger import Logger
import time

from conftest import *
from business.tiku.tiku_page import TiKu_Page as TK
#搜索页面对象
class TiKu_bs():
    def __init__(self,drivers):

        Logger.info(drivers)
        self.d0=BasePage(drivers[0])
        #self.d1 = BasePage(drivers[1]) #多设备交互时使用这个,单设备执行注掉
        #self.drivers=drivers

    def open_TiKu(self,parameter):
        runtime = time.strftime("%Y%m%d%H%M%S", time.localtime())
        screenrecord_name='%s_open_TiKu_test'%runtime
        #self.d0.screenrecord_start(screenrecord_name,doc='开始录屏……')  # pip3 install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple
        Package_name="com.gsx.tiku"
        self.d0.clear(Package_name, doc='清理“%s”缓存数据'%(Package_name))
        self.d0.open(Package_name,stop=True,doc='打开“%s”APP'%(Package_name))
        self.d0.clicks(*TK.tv_agree, doc='点击切换为密码登录')
        if self.d0.locate(*TK.ShanYanOneKey,doc='判断其他登录方式是否存在',number=8,exists_type=2):
            self.d0.clicks(*TK.ShanYanOneKey, doc='其他登录方式存在,切换为其他方式登录')
        self.d0.clicks(*TK.tv_login_pwd,doc='点击切换为密码登录')
        self.d0.input(*TK.LoginID,parameter['id'],doc='输入用户id账户')
        self.d0.input(*TK.LoginPWD, parameter['pwd'], doc='输入用户登录密码')
        self.d0.clicks(*TK.iv_select,  doc='勾选用户协议')
        self.d0.clicks(*TK.bt_login, doc='点击登录')
        self.d0.clicks(*TK.tvName, doc='点击我的')
        #self.d0.screenrecord_sotp( screenrecord_name,doc='结束录屏……')








#!/user/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# @File : tiku_assert.py
# @Time : 2022-06-01 17:25
# @Author : mojin
# @Email : 397135766@qq.com
# @Software : PyCharm
#-------------------------------------------------------------------------------

from base.base_page import BasePage
from business.tiku.tiku_page import  TiKu_Page as TK
from common.logger import Logger

class TiKu_Assert:

    def __init__(self, drivers):
        #self.driver = driver
        self.d0 = BasePage(drivers[0])

    def tiku_assert(self, expect_text,screenrecord_name_list):
        result=[
            self.d0.assert_text(*TK.tvNickName, expect_text[0], screenrecord_name_list[0],doc="断言:登录的账户id和获取的id是否相等" ),
        ]
        #self.d0.screenrecord_sotp(screenrecord_name_list[0], doc='添加录屏')
        return False not in result


#!/user/bin/env python3
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------------------
# @File : test_001.py
# @Time : 2022-04-21 12:02
# @Author : mojin
# @Email : 397135766@qq.com
# @Software : PyCharm
# -------------------------------------------------------------------------------

import multiprocessing as np
import pytest, allure,os
import json
import time
from business.tiku.tiku_bs import TiKu_bs
from business.tiku.tiku_assert import TiKu_Assert
from common.get_devices import get_devices_app_package
from common.logger import Logger


@allure.epic("TiKu")  # 项目名称
@allure.feature("TiKu打开")  # 一级标签
@allure.story('TiKu测试')  # 二级标签
class Test_Case():
    '''
    用例级别
    #blocker,critical,normal,minor,trivial  阻塞,严重,一般,次要,轻微
    @allure.severity装饰器按严重性级别来标记case  @allure.severity('minor')
    allure对用例的等级划分成五个等级
    blocker 阻塞缺陷(功能未实现,无法下一步)
    critical 严重缺陷(功能点缺失)
    normal 一般缺陷(边界情况,格式错误)
    minor 次要缺陷(界面错误与ui需求不符)
    trivial 轻微缺陷(必须项无提示,或者提示不规范)

    '''

    # @pytest.mark.parametrize("case", range(20))#参数化
    #@pytest.mark.skip(reason="就是不想执行而已")  # 不执行次用例
    # @pytest.mark.skipif(condition='1<2', reason='feature not implemented') 条件跳过
    # @pytest.mark.run(order=3) 自定义测试顺序,顺序为:1,2,3,无标记,-3,-2,-1
    # @pytest.mark.xfail(condition='1==2', reason="The test case")     #  condition 条件不等判断成功
    # @pytest.mark.smoke##运行时加上命令‘-m=smoke’ #@pytest.mark.login  # 用例标记
    # @pytest.mark.flaky(reruns=2, reruns_delay=10) 失败重跑

    #@pytest.mark.repeat(1)  # 用例重复测试 次数

    # @allure.title('测试')  # 三级标签,这是在用例方法上面添加用例标题,但不能引用用例传进来的变量
    @allure.severity('blocker')  # 用例等级
    @allure.issue('https://www.baidu.com/', "BUG号:123")  # 问题表识,关联标识已有的问题,可为一个url链接地址
    @allure.testcase('https://www.baidu.com/', "用例名:测试字符串相等")  # url, name=None 用例标识,关联标识用例,可为一个url链接地址
    @allure.description('用例描述……')  # 用例描述
    @allure.link('https://www.baidu.com/', name="关联的连接")  # 关联的连接
    @allure.step('test_001')
    #@pytest.mark.skip(reason="就是不想执行而已")  # 不执行此用例
    def test_001(self, group_data):
        # screenrecord_name_list=open_iphone[0]
        # drivers, div = drivers  # 给drivers和div赋值 ['devices_list']
        #allure.dynamic.title('测试-%s' % (div))  # 这个是用例方法内部添加用例标题的,可以引用用例传进来的变量参数,在用例标题中显示设备编号
        parameter = group_data['test_001']['Test_Case']['test_open_TiKu']  # 读取到test_Call用例源数据
        # parameter=data
        Logger.info(parameter)
        with allure.step('步骤图片'):
            allure.attach.file("config/diti (18).jpg", name="需要查看的图片",
                           attachment_type=allure.attachment_type.JPG)  # 测试步骤中添加一张图片或视频


        with allure.step('步骤1'):
            allure.attach(
                json.dumps('这是一个allure报告示例', ensure_ascii=False, indent=4),
                "附件内容",
                allure.attachment_type.TEXT,
            )
        with allure.step('资源图'):
            allure.attach.file('config/test/group_data_1.yaml', name="插入yaml文件数据",
                               attachment_type=allure.attachment_type.YAML)  # 测试步骤中添加一张图片或视频
            allure.attach.file('config/45_.mp4', name="插入MP4文件",
                               attachment_type=allure.attachment_type.MP4)  # 测试步骤中添加一张图片或视频
            allure.attach.file('config/allure_3x2_qianqian.mp3', name="插入MP3文件",
                               attachment_type=allure.attachment_type.MP4)  # 测试步骤中添加一张图片或视频
        with allure.step('打印json数据'):
            allure.attach(
                json.dumps(group_data, ensure_ascii=False, indent=4),
                "附件内容",
                allure.attachment_type.JSON,
            )

        with allure.step('打印列表数据'):
            allure.attach(
                json.dumps(parameter, ensure_ascii=False, indent=4),
                "附件内容",
                allure.attachment_type.JSON,
            )
        pytest.assume(4==2)
        pytest.assume(4 ==4)


    @allure.severity('blocker')  # 用例等级
    @allure.issue('https://www.baidu.com/', "BUG号:123")  # 问题表识,关联标识已有的问题,可为一个url链接地址
    @allure.testcase('https://www.baidu.com/', "用例名:测试字符串相等")  # url, name=None 用例标识,关联标识用例,可为一个url链接地址
    @allure.description('用例描述……')  # 用例描述
    @allure.link('https://www.baidu.com/', name="关联的连接")  # 关联的连接
    @allure.step('TiKu测试')
    #@pytest.mark.skip('跳过')
    def test_open_TiKu(self, drivers, group_data, open_iphone):
            screenrecord_name_list=open_iphone[0]
            drivers, div = drivers  # 给drivers和div赋值 ['devices_list']
            allure.dynamic.title('登录-%s' % (div))  # 这个是用例方法内部添加用例标题的,可以引用用例传进来的变量参数,在用例标题中显示设备编号
            parameter = group_data['test_001']['Test_Case']['test_open_TiKu']  # 读取到test_Call用例源数据
            # parameter=data
            Logger.info(parameter)
            TiKu_bs(drivers).open_TiKu(parameter)


            assert TiKu_Assert(drivers).tiku_assert([parameter['id']],screenrecord_name_list)

        #
        #





三. 测试报告

在这里插入图片描述

项目截图

项目截图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,下面是一个简单的 Python + Pytest + Allure 实现接口自动化 POM 框架的示例: 1. 安装依赖库 ``` pip install requests pip install pytest pip install allure-pytest ``` 2. 创建项目目录结构: ``` ├── api │ ├── __init__.py │ ├── base_api.py │ └── user_api.py ├── common │ ├── __init__.py │ ├── constants.py │ ├── logger.py │ └── utils.py ├── config │ ├── __init__.py │ └── config.py ├── data │ ├── __init__.py │ └── users.json ├── pytest.ini ├── README.md ├── requirements.txt ├── testcases │ ├── __init__.py │ ├── conftest.py │ ├── test_login.py │ └── test_user.py └── pytest.ini ``` 3. 编写配置文件 `config.py`、常量文件 `constants.py` 等。 `config.py`: ```python class Config: API_BASE_URL = 'https://xxx.com/api' USERNAME = 'testuser' PASSWORD = 'testpass' ``` `constants.py`: ```python class StatusCode: SUCCESS = 200 CLIENT_ERROR = 400 SERVER_ERROR = 500 ``` 4. 创建 HTTP 请求封装类 `base_api.py` 和业务接口类 `user_api.py` `base_api.py`: ```python import requests from common.logger import logger from common.constants import StatusCode from config.config import Config class BaseApi: def __init__(self): self.session = requests.session() self.base_url = Config.API_BASE_URL def request(self, method, url, **kwargs): url = self.base_url + url response = self.session.request(method, url, **kwargs) logger.info(f'{method} {url} {kwargs} response: {response.json()}') return response def get(self, url, params=None, **kwargs): return self.request('get', url, params=params, **kwargs) def post(self, url, data=None, json=None, **kwargs): return self.request('post', url, data=data, json=json, **kwargs) def put(self, url, data=None, **kwargs): return self.request('put', url, data=data, **kwargs) def delete(self, url, **kwargs): return self.request('delete', url, **kwargs) def assert_status_code(self, response, expected_status_code): assert response.status_code == expected_status_code, \ f'Expected status code is {expected_status_code}, but actual is {response.status_code}' response_json = response.json() assert response_json['code'] == StatusCode.SUCCESS, \ f'Response code is {response_json["code"]}, message is {response_json["message"]}' ``` `user_api.py`: ```python from api.base_api import BaseApi from common.constants import StatusCode from config.config import Config from common.utils import json_load class UserApi(BaseApi): def __init__(self): super().__init__() def login(self): url = '/login' data = { 'username': Config.USERNAME, 'password': Config.PASSWORD } response = self.post(url, json=data) self.assert_status_code(response, StatusCode.SUCCESS) return response.json()['data']['access_token'] def get_user_info(self, user_id): url = f'/users/{user_id}' headers = { 'Authorization': f'Bearer {self.login()}' } response = self.get(url, headers=headers) self.assert_status_code(response, StatusCode.SUCCESS) return response.json()['data'] def create_user(self, data): url = '/users' headers = { 'Authorization': f'Bearer {self.login()}' } response = self.post(url, json=data, headers=headers) self.assert_status_code(response, StatusCode.SUCCESS) return response.json()['data'] def delete_user(self, user_id): url = f'/users/{user_id}' headers = { 'Authorization': f'Bearer {self.login()}' } response = self.delete(url, headers=headers) self.assert_status_code(response, StatusCode.SUCCESS) return response.json()['data'] def get_random_user(self): users = json_load('data/users.json') return users[0] ``` 5. 编写测试用例 `test_user.py` 和 `test_login.py` `test_user.py`: ```python import pytest from api.user_api import UserApi class TestUser: @pytest.fixture(scope='class') def user(self): return UserApi().get_random_user() def test_create_and_delete_user(self, user): user_api = UserApi() new_user = user_api.create_user(user) assert new_user['username'] == user['username'] user_api.delete_user(new_user['id']) def test_get_user_info(self): user_api = UserApi() user_info = user_api.get_user_info(1) assert user_info['username'] == 'admin' ``` `test_login.py`: ```python import pytest from api.user_api import UserApi class TestLogin: def test_login(self): user_api = UserApi() access_token = user_api.login() assert access_token is not None ``` 6. 运行测试用例 在项目根目录下执行以下命令: ``` pytest --alluredir=./allure-results testcases/ ``` 7. 生成报告 在项目根目录下执行以下命令: ``` allure serve ./allure-results ``` 此时会启动一个本地服务,打开浏览器输入 `http://localhost:port` 即可查看测试报告。 以上就是一个简单的 Python + Pytest + Allure 实现接口自动化 POM 框架的示例,希望对你有帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值