框架设计
pytest
selenium
POM页面对象模型(Page Object Model)
目录结构
代码语言:javascript复制common ——公共类
Page ——基类
PageElements ——页面元素类
PageObject ——页面对象类
TestCase ——测试用例
conftest.py ——pytest前后置配置文件
pytest.ini ——pytest配置文件
添加配置文件
配置文件总是项目中必不可少的部分!
将固定不变的信息集中在固定的文件中
conf.py
项目中都应该有一个文件对整体的目录进行管理,我也在这个python项目中设置了此文件。
在项目config
目录创建conf.py
文件,所有的目录配置信息写在这个文件里面。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
from selenium.webdriver.common.by import By
from utils.times import dt_strftime
class ConfigManager(object):
# 项目目录
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 页面元素目录
ELEMENT_PATH = os.path.join(BASE_DIR, 'page_element')
# 报告文件
REPORT_FILE = os.path.join(BASE_DIR, 'report.html')
#元素文件
DATA_PATH = os.path.join (BASE_DIR, 'page_element', 'data.xlsx')
#配置目录
CONFIG_PATH = os.path.join (BASE_DIR, "config", "config.ini")
#谷歌浏览器驱动
CHROMEDRIVER_PATH = os.path.join (BASE_DIR, "resource","chromedriver.exe")
# 邮件信息
EMAIL_INFO = {
'username': '553187951@163.com', # 切换成你自己的地址
'password': 'HGCXVUSRONKBRLTS',
'smtp_host': 'smtp.163.com',
'smtp_port': 465
}
# 收件人
ADDRESSEE = [
'553187951@qq.com',
]
@property
def screen_path(self):
"""截图目录"""
screenshot_dir = os.path.join(self.BASE_DIR, 'screen_capture')
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
now_time = dt_strftime("%Y%m%d%H%M%S")
screen_file = os.path.join(screenshot_dir, "{}.png".format(now_time))
return now_time, screen_file
@property
def log_file(self):
"""日志目录"""
log_dir = os.path.join(self.BASE_DIR, 'logs')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
return os.path.join(log_dir, '{}.log'.format(dt_strftime()))
@property
def ini_file(self):
"""配置文件"""
ini_file = self.CONFIG_PATH
if not os.path.exists(ini_file):
raise FileNotFoundError("配置文件%s不存在!" % ini_file)
return ini_file
cm = ConfigManager()
if __name__ == '__main__':
print(cm.BASE_DIR)
在这个文件中我们可以设置自己的各个目录,也可以查看自己当前的目录。
遵循了约定:不变的常量名全部大写,函数名小写。看起来整体美观。
config.ini
在项目config
目录新建一个config.ini
文件,里面暂时先放入我们的需要测试的host和url
[HOST]
host = https://account.369zhan.com
url=/auth/loginPage?platformName=智奥中智兴主场服务平台&tokenUrl=https://zhan.zzxes.com.cn/#/
读取配置文件
配置文件创建好了,接下来我们需要读取这个配置文件以使用里面的信息。
我们在common
目录中新建一个readconfig.py
文件
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import configparser
from config.conf import cm
class ReadConfig(object):
"""配置文件"""
def __init__(self):
self.config = configparser.RawConfigParser() # 当有%的符号时请使用Raw读取
self.config.read(cm.ini_file, encoding='utf-8')
def _get(self, section, option):
"""获取"""
return self.config.get(section, option)
def _set(self, section, option, value):
"""更新"""
self.config.set(section, option, value)
with open(cm.ini_file, 'w') as f:
self.config.write(f)
def url(self,HOST, key):
return self._get(HOST, key)
ini = ReadConfig()
if __name__ == '__main__':
print(ini.url("HOST","host") ini.url("HOST","url"))
用python内置的configparser模块对config.ini
文件进行了读取。
对于url值的提取,使用了@property
属性值,写法更简单。
管理时间
因为很多的模块会用到时间戳,或者日期等等字符串,所以我们先单独把时间封装成一个模块。
然后让其他模块来调用即可。在utils目录新建times.py模块
代码语言:javascript复制#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import time
import datetime
from functools import wraps
def timestamp():
"""时间戳"""
return time.time()
def dt_strftime(fmt="%Y%m"):
"""
datetime格式化时间
:param fmt "%Y%m%d %H%M%S
"""
return datetime.datetime.now().strftime(fmt)
def sleep(seconds=1.0):
"""
睡眠时间
"""
time.sleep(seconds)
def running_time(func):
"""函数运行时间"""
@wraps(func)
def wrapper(*args, **kwargs):
start = timestamp()
res = func(*args, **kwargs)
print("校验元素done!用时%.3f秒!" % (timestamp() - start))
return res
return wrapper
if __name__ == '__main__':
print(dt_strftime("%Y%m%d%H%M%S"))
记录操作日志
日志,大家应该都很熟悉这个名词,就是记录代码中的动作。
在utils
目录中新建logger.py
文件。
这个文件就是我们用来在自动化测试过程中记录一些操作步骤的。
代码语言:javascript复制#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import logging
from config.conf import cm
class Log:
def __init__(self):
self.logger = logging.getLogger()
if not self.logger.handlers:
self.logger.setLevel(logging.DEBUG)
# 创建一个handle写入文件
fh = logging.FileHandler(cm.log_file, encoding='utf-8')
fh.setLevel(logging.INFO)
# 创建一个handle输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# 定义输出的格式
formatter = logging.Formatter(self.fmt)
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 添加到handle
self.logger.addHandler(fh)
self.logger.addHandler(ch)
@property
def fmt(self):
return '%(levelname)st%(asctime)st[%(filename)s:%(lineno)d]t%(message)s'
log = Log().logger
if __name__ == '__main__':
log.info('hello world')
在终端中运行该文件,就看到命令行打印出了:
代码语言:javascript复制INFO 2020-12-01 16:00:05,467 [logger.py:38] hello world
然后在项目logs目录下生成了当月的日志文件。
pytest 输出日志需要在配置文件中添加参数
代码语言:javascript复制log_cli = 1
log_cli_level = INFO
log_cli_format = %(asctime) s [%(levelname) 8s] %(message) s (%(filename) s:%(lineno) s)
log_cli_date_format=%Y-%m-%d %H:%M:%S
简单理解POM模型
由于下面要讲元素相关的,所以首先理解一下POM模型
Page Object模式具有以下几个优点。
- 抽象出对象可以最大程度地降低开发人员修改页面代码对测试的影响, 所以, 你仅需要对页 面对象进行调整, 而对测试没有影响;
- 可以在多个测试用例中复用一部分测试代码;
- 测试代码变得更易读、 灵活、 可维护
Page Object模式图
- basepage ——selenium的基类,对selenium的方法进行封装
- pageelements——页面元素,把页面元素单独提取出来,放入一个文件中
- searchpage ——页面对象类,把selenium方法和页面元素进行整合
- testcase ——使用pytest对整合的searchpage进行测试用例编写
通过上图我们可以看出,通过POM模型思想,我们把:
- selenium方法
- 页面元素
- 页面对象
- 测试用例
以上四种代码主体进行了拆分,虽然在用例很少的情况下做会增加代码,但是当用例多的时候意义很大,代码量会在用例增加的时候显著减少。我们维护代码变得更加直观明显,代码可读性也变得比工厂模式强很多,代码复用率也极大的得到了提高。
元素定位
xpath
语法规则
[菜鸟教程](https://www.runoob.com/xpath/xpath-intro.html)中对于 xpath 的介绍是一门在 XML 文档中查找信息的语言。
表达式 | 介绍 | 备注 |
---|---|---|
/ | 根节点 | 绝对路径 |
// | 当前节点的所有子节点 | 相对路径 |
* | 所有节点元素的 | |
@ | 属性名的前缀 | @class @id |
*[1] | [] 下标运算符 | |
[] | [ ]谓词表达式 | //input[@id='kw'] |
Following-sibling | 当前节点之后的同级 | |
preceding-sibling | 当前节点之前的同级 | |
parent | 当前节点的父级节点 |
定位工具
- chropath
管理页面元素
项目框架设计中有一个目录page_element
就是专门来存放定位元素的文件的。
通过对各种配置文件的对比,我在这里选择的是excel文件。其易读,交互性好。
page_element
中新建一个data.xlxs
文件。
在common
目录中创建ParseExcel.py
文件。
import logging
from openpyxl import load_workbook
from config.conf import cm
class ParseExcel(object):
def __init__(self):
self.wk = load_workbook(cm.DATA_PATH)
self.excelFile = cm.DATA_PATH
def get_sheet_first(self):
"""获取sheet对象"""
sheet = self.wk[0]
return sheet
def get_sheet_by_name(self, sheet_name):
"""获取sheet对象"""
sheet = self.wk[sheet_name]
return sheet
def get_row_num(self, sheet):
"""获取有效数据的最大行号"""
return sheet.max_row
def get_cols_num(self, sheet):
"""获取有效数据的最大列号"""
return sheet.max_column
def get_row_values(self, sheet, row_num):
"""获取某一行的数据"""
max_cols_num = self.get_cols_num(sheet)
row_values = []
for colsNum in range(1, max_cols_num 1):
value = sheet.cell(row_num, colsNum).value
if value is None:
value = ''
row_values.append(value)
return tuple(row_values)
def get_column_values(self, sheet, column_num):
"""获取某一列的数据"""
max_row_num = self.get_row_num(sheet)
column_values = []
for rowNum in range(2, max_row_num 1):
value = sheet.cell(rowNum, column_num).value
if value is None:
value = ''
column_values.append(value)
return tuple(column_values)
def get_value_of_cell(self, sheet, row_num, column_num):
"""获取某一个单元格的数据"""
value = sheet.cell(row_num, column_num).value
if value is None:
value = ''
return value
def get_all_values_of_sheet(self, sheet):
"""获取某一个sheet页的所有测试数据,返回一个元祖组成的列表"""
max_row_num = self.get_row_num(sheet)
column_num = self.get_cols_num(sheet)
all_values = []
for row in range(2, max_row_num 1):
row_values = []
for column in range(1, column_num 1):
value = sheet.cell(row, column).value
if value is None:
value = ''
row_values.append(value)
all_values.append(tuple(row_values))
return all_values
if __name__ == '__main__':
key="lp_loginUsername"
exe=ParseExcel()
sheet=exe.get_sheet_by_name('element')
row = exe.get_row_num (sheet)
try:
for i in range(row):
value = exe.get_value_of_cell (sheet,i 1,2)
if value==key:
LocatorValue=exe.get_value_of_cell (sheet,i 1,4)
print (LocatorValue)
except Exception:
pass
然后就可以读取对应的元素。
封装Selenium基类
代码语言:javascript复制#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
selenium基类
本文件存放了selenium基类的封装方法
"""
import logging
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from common.ParseExcelFile import ParseExcel
from config.conf import cm
from utils.logger import log
from utils.times import sleep
class basepage(object):
"""selenium基类"""
def __init__(self, driver):
# self.driver = webdriver.Chrome()
self.driver = driver
self.timeout = 20
self.wait = WebDriverWait(self.driver, self.timeout)
def get_url(self, url):
"""打开网址并验证"""
self.driver.maximize_window()
self.driver.set_page_load_timeout(60)
try:
self.driver.get(url)
self.driver.implicitly_wait(10)
log.info("打开网页:%s" % url)
except TimeoutException:
raise TimeoutException("打开%s超时请检查网络或网址服务器" % url)
#通过excel读取方式获取元素定位方式,name,id,xpath等
def getlocatorBy(self,key):
global locatorBy
excel = ParseExcel ()
sheet = excel.get_sheet_by_name ('element')
row = excel.get_row_num(sheet)
try:
for i in range (row):
value = excel.get_value_of_cell (sheet, i 1, 2)
if value == key:
locatorBy = excel.get_value_of_cell (sheet, i 1, 3)
break
except Exception:
pass
return locatorBy
# 通过读取excel中的数据获取元素定位值
def getlocatorValue(self,key):
global locatorValue
excel = ParseExcel ()
sheet = excel.get_sheet_by_name ('element')
row = excel.get_row_num(sheet)
try:
for i in range (row):
value = excel.get_value_of_cell (sheet, i 1, 2)
if value == key:
locatorValue = excel.get_value_of_cell (sheet, i 1, 4)
break
except Exception:
pass
return locatorValue
# 通过读取excel中的key,以元祖的形式输入定位方式和值。
def getByLocal(self, key):
locatorBy= self.getlocatorBy (key)
locatorValue=self.getlocatorValue(key)
if locatorBy == 'name':
return By.NAME, locatorValue
elif locatorBy == 'id':
return By.ID, locatorValue
elif locatorBy == 'xpath':
return By.XPATH, locatorValue
elif locatorBy == 'link_text':
return By.LINK_TEXT, locatorValue
elif locatorBy == 'class_name':
return By.CLASS_NAME, locatorValue
else:
print("暂未定义此定位元素方式")
#js的定位封装
def getElement_js(self,key):
locatorBy = self.getlocatorBy (key)
locatorValue = self.getlocatorValue (key)
log.info ("你的定位信息的方式为" locatorBy);
log.info ("你的定位信息的值为" locatorValue);
if locatorBy=="id":
return self.driver.execute_script("return document.getElementById('%s')"%locatorValue)
#如果是name,tag、classname的话返回多个元素对象的话,默认操作第一个
elif locatorBy=="name":
return self.driver.execute_script("return document.getElementsByName('%s')"%locatorValue)[0]
elif locatorBy=="tag":
return self.driver.execute_script("return document.getElementsByTagName('%s')"%locatorValue)[0]
elif locatorBy=="css":
pass
else:
log.info("定位方式不支持!")
#显示等待定位单个元素方法方法
def getElement(self,key):
return WebDriverWait (self.driver, 10).until (lambda driver: driver.find_element (*(self.getByLocal(key))))# 判断元素是否存在
#显示等待定位多个元素方法方法
def getElements(self,key):
return WebDriverWait (self.driver, 10).until (lambda driver: driver.find_elements(*(self.getByLocal(key))))# 判断元素集合是否存在
# return self.driver.find_elements(*(self.getByLocal(key)))
#通过文本直接定位对应元素
def getElement_containstext(self,key):
return self.driver.findElement(By.xpath("//*[contains(text(),'" self.getlocatorValue(key) "')]"));
# 打开网页
def open_url(self, url):
log.info("打开地址" url)
self.driver.get(url)
# 文本框输入数据
def send(self, element, key):
self.getElement(element).send_keys(key)
def click(self, element):
self.getElement(element).click()
def clear(self, element):
self.getElement(element).clear()
def sleep(self,sec):
time.sleep(sec)
def get_txt(self,key):
"""
方法用于获取元素文本值
"""
_text = self.getElement(key).text
return _text
def forward (self):
"""浏览器前进"""
self.driver.forward ()
def back (self):
"""浏览器后退"""
self.driver.back ()
if __name__ == "__main__":
pass
创建页面对象
在page_object
目录下创建一个LoginPage.py
文件。
from page.basepage import basepage
class LoginPage(basepage):
#
def __init__ (self, driver):
self.driver = driver
def get_loginUsername(self):
return self.getElement("lp_loginUsername")
def get_loginPassword(self):
return self.getElement("lp_loginPassword")
def get_loginButton(self):
return self.getElement("lp_loginButton")
def send_username(self,key):
element=self.get_loginUsername()
self.clear(element)
self.send(element,key)
def send_password(self,key):
element = self.get_loginPassword ()
self.clear(element)
self.send (element, key)
def click_loginButton(self):
element=self.get_loginButton()
self.click(element)
def Login(self,username,password):
self.send_username(username)
self.send_password(password)
self.click_loginButton()
下面开始编写测试用例。熟悉一下pytest测试框架。
简单了解Pytest
打开pytest框架的官网。http://www.pytest.org/en/latest/
代码语言:javascript复制# content of test_sample.py
def inc(x):
return x 1
def test_answer():
assert inc(3) == 5
推荐看一下[上海悠悠的pytest教程](https://www.cnblogs.com/yoyoketang/tag/pytest/)。
pytest.ini
pytest项目中的配置文件,可以对pytest执行过程中操作做全局控制。
在项目根目录新建pytest.ini
文件。
[pytest]
addopts = --html=report.html --self-contained-html
- addopts 指定执行时的其他参数说明:
--html=report/report.html --self-contained-html
生成pytest-html带样式的报告-s
输出用例中的调式信息-q
安静的进行测试-v
可以输出用例更加详细的执行信息,比如用例所在的文件及用例名称等
编写测试用例
使用pytest编写测试用例。
在TestCase
目录中创建test_login.py
文件。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import re
import pytest
import allure
from page_object.LoginPage import LoginPage
from common.readconfig import ini
from utils.logger import log
@allure.feature("测试登录模块")
class TestLogin:
@pytest.fixture(scope='function', autouse=True)
def open_browser(self, drivers):
l = LoginPage (drivers)
l.get_url(ini.url("HOST","host") ini.url("HOST","url"))
@allure.story("测试登录用例")
@pytest.mark.smoke
@pytest.mark.parametrize ('username, password',[ ('13129562261', 'czh123')])
def test_login(self, username, password,drivers):
l = LoginPage (drivers)
l.Login(username,password)
if __name__ == '__main__':
pytest.main()
我们测试用了就编写好了。
- pytest.fixture 这个实现了和unittest的setup,teardown一样的前置启动,后置清理的装饰器。
main方法中为执行启动的语句。
这时候我们应该进入执行了,但是还有一个问题,我们还没有把driver传递。
conftest.py
我们在项目根目录下新建一个conftest.py
文件。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import base64
import logging
import os
import pytest
import allure
from py.xml import html
from selenium import webdriver
from config.conf import cm
from common.readconfig import ini
from script.addpath import BASE_DIR
from utils.times import timestamp
from utils.send_mail import send_report
driver = None
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@pytest.fixture(scope='session', autouse=True)
def drivers(request):
global driver
if driver is None:
option = webdriver.ChromeOptions ()
option.headless = False
driver = webdriver.Chrome(executable_path =cm.CHROMEDRIVER_PATH,options = option)
driver.maximize_window()
def fn():
driver.quit()
request.addfinalizer(fn)
return driver
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item):
"""
当测试失败的时候,自动截图,展示到html报告中
:param item:
"""
pytest_html = item.config.pluginmanager.getplugin('html')
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__)
extra = getattr(report, 'extra', [])
if report.when == 'call' or report.when == "setup":
xfail = hasattr(report, 'wasxfail')
if (report.skipped and xfail) or (report.failed and not xfail):
screen_img = _capture_screenshot()
if screen_img:
html = '<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:1024px;height:768px;" '
'onclick="window.open(this.src)" align="right"/></div>' % screen_img
extra.append(pytest_html.extras.html(html))
report.extra = extra
def pytest_html_results_table_header(cells):
cells.insert(1, html.th('用例名称'))
cells.insert(2, html.th('Test_nodeid'))
cells.pop(2)
def pytest_html_results_table_row(report, cells):
cells.insert(1, html.td(report.description))
cells.insert(2, html.td(report.nodeid))
cells.pop(2)
def pytest_html_results_table_html(report, data):
if report.passed:
del data[:]
data.append(html.div('通过的用例未捕获日志输出.', class_='empty log'))
def pytest_html_report_title(report):
report.title = "pytest示例项目测试报告"
def pytest_configure(config):
config._metadata.clear()
config._metadata['测试项目'] = "测试登录"
config._metadata['测试地址'] = ini.url
def pytest_html_results_summary(prefix, summary, postfix):
# prefix.clear() # 清空summary中的内容
prefix.extend([html.p("所属公司: 九象展览科技")])
prefix.extend([html.p("测试执行人: czh")])
def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""收集测试结果"""
result = {
"total": terminalreporter._numcollected,
'passed': len(terminalreporter.stats.get('passed', [])),
'failed': len(terminalreporter.stats.get('failed', [])),
'error': len(terminalreporter.stats.get('error', [])),
'skipped': len(terminalreporter.stats.get('skipped', [])),
# terminalreporter._sessionstarttime 会话开始时间
'total times': timestamp() - terminalreporter._sessionstarttime
}
print(result)
if result['failed'] or result['error']:
send_report()
def _capture_screenshot():
"""截图保存为base64"""
now_time, screen_file = cm.screen_path
driver.save_screenshot(screen_file)
allure.attach.file(screen_file,
"失败截图{}".format(now_time),
allure.attachment_type.PNG)
with open(screen_file, 'rb') as f:
imagebase64 = base64.b64encode(f.read())
return imagebase64.decode()
conftest.py测试框架pytest的胶水文件,里面用到了fixture的方法,封装并传递出了driver。
执行用例
以上我们已经编写完成了整个框架和测试用例。
我们进入到当前项目的主目录执行命令:
代码语言:javascript复制pytest
命令行输出:
代码语言:javascript复制============================= test session starts =============================
platform win32 -- Python 3.8.5, pytest-6.2.4, py-1.9.0, pluggy-0.13.1
rootdir: E:workspacene_p_uitest, configfile: pytest.ini
plugins: allure-pytest-2.8.40, html-3.1.1, metadata-1.11.0, rerunfailures-9.1.1
collected 1 item
TestCase/test_login.py::TestLogin::test_login[13129562261-czh123]
------------------------------- live log setup --------------------------------
2022-02-08 18:33:09 [ INFO] 打开网页:https://account.369zhan.com/auth/loginPage?platformName=智奥中智兴主场服务平台&tokenUrl=https://zhan.zzxes.com.cn/#/ (basepage.py:37)
PASSED
------ generated html file: file://E:workspacene_p_uitestreport.html -------
{'total': 1, 'passed': 1, 'failed': 0, 'error': 0, 'skipped': 0, 'total times': 9.162506341934204}
============================== 1 passed in 9.16s ==============================
Report successfully generated to allure-report
项目的report目录中生成了一个report.html文件。
这就是生成的测试报告文件。
发送邮件
当项目执行完成之后,需要发送到自己或者其他人邮箱里查看结果。
我们编写发送邮件的模块。
在utils
目录中新建send_mail.py
文件
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import zmail
from config.conf import cm
def send_report():
"""发送报告"""
with open(cm.REPORT_FILE, encoding='utf-8') as f:
content_html = f.read()
try:
mail = {
'from': '553187951@163.com',
'subject': '最新的测试报告邮件',
'content_html': content_html,
'attachments': [cm.REPORT_FILE, ]
}
server = zmail.server(*cm.EMAIL_INFO.values())
server.send_mail(cm.ADDRESSEE, mail)
print("测试邮件发送成功!")
except Exception as e:
print("Error: 无法发送邮件,{}!", format())
if __name__ == "__main__":
'''请先在config/conf.py文件设置QQ邮箱的账号和密码'''
send_report()
执行该文件:
代码语言:javascript复制测试邮件发送成功!
运行
安装依赖
代码语言:javascript复制pip install -r requirements.txt
执行主文件
- 在项目根目录执行
run_case.py
文件即可运行项目
allure参数说明
- pytest --alluredir
result-path
- --clean-alluredir 清除历史生成记录
- allure generate
result-path
- -c 生成报告前删除上一次生成的报告
- -o 指定生成的报告目录
- allure open
report-path
持续集成:
在jekins上新建一个自由风格项目,然后进入项目配置页面:
1.设置工作做目录
2.配置构建命令
然后 插件管理处添加allure插件,
在Global Tool Configuration中去加入allure相关路径配置
回到项目的配置页面,添加相关配置,主要是resultpath和reportpath
完成,此时项目构建完成后,会有对应的报告展示区域,点击后跳转至allure报告页
最后附上代码地址:
纳尼/MyPythonUItest (gitee.com)