# MyPythonUItest **Repository Path**: czhtest/MyPythonUItest ## Basic Information - **Project Name**: MyPythonUItest - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-01-19 - **Last Updated**: 2022-03-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # web-UI 自动化测试示例 --- ## 框架设计 - pytest - selenium - POM页面对象模型(Page Object Model) --- ## 目录结构 common ——公共类 Page ——基类 PageElements ——页面元素类 PageObject ——页面对象类 TestCase ——测试用例 conftest.py ——pytest胶水文件 pytest.ini ——pytest配置文件 --- ## 管理时间 因为很多的模块会用到时间戳,或者日期等等字符串,所以我们先单独把时间封装成一个模块。 然后让其他模块来调用即可。在`utils`目录新建`times.py`模块 ```python #!/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")) ``` ------ ## 添加配置文件 配置文件总是项目中必不可少的部分! 将固定不变的信息集中在固定的文件中 ### conf.py[#](https://www.cnblogs.com/wxhou/p/12207914.html#confpy) > 项目中都应该有一个文件对整体的目录进行管理,我也在这个python项目中设置了此文件。 在项目`config`目录创建`conf.py`文件,所有的目录配置信息写在这个文件里面。 ```python #!/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 ```ini [HOST] host = https://account.369zhan.com url=/auth/loginPage?platformName=%E6%99%BA%E5%A5%A5%E4%B8%AD%E6%99%BA%E5%85%B4%E4%B8%BB%E5%9C%BA%E6%9C%8D%E5%8A%A1%E5%B9%B3%E5%8F%B0&tokenUrl=https%3A%2F%2Fzhan.zzxes.com.cn%2F%23%2F ``` ### 读取配置文件[#](https://www.cnblogs.com/wxhou/p/12207914.html#读取配置文件) 配置文件创建好了,接下来我们需要读取这个配置文件以使用里面的信息。 我们在`common`目录中新建一个`readconfig.py`文件 ```python #!/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`目录中新建`logger.py`文件。 这个文件就是我们用来在自动化测试过程中记录一些操作步骤的。 ```python #!/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)s\t%(asctime)s\t[%(filename)s:%(lineno)d]\t%(message)s' log = Log().logger if __name__ == '__main__': log.info('hello world') ``` 在终端中运行该文件,就看到命令行打印出了: ```shell INFO 2020-12-01 16:00:05,467 [logger.py:38] hello world ``` 然后在项目logs目录下生成了当月的日志文件。 pytest 输出日志需要在配置文件中添加参数 ``` 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模式图** [![img](https://tva1.sinaimg.cn/large/006tNbRwly1gayg3p0meuj30ag0abt90.jpg)](https://tva1.sinaimg.cn/large/006tNbRwly1gayg3p0meuj30ag0abt90.jpg) - 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`文件。 image-20220208165710432 在`common`目录中创建`ParseExcel.py`文件。 ```python 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基类 ```python #!/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`文件。 ```python 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/ ```python # 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`文件。 ```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`文件。 ```python #!/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`文件。 ```python #!/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 = '
screenshot
' % 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。 ------ ## 执行用例 以上我们已经编写完成了整个框架和测试用例。 我们进入到当前项目的主目录执行命令: ```powershell pytest ``` 命令行输出: ```powershell ============================= test session starts ============================= platform win32 -- Python 3.8.5, pytest-6.2.4, py-1.9.0, pluggy-0.13.1 rootdir: E:\workspace\ne_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=%E6%99%BA%E5%A5%A5%E4%B8%AD%E6%99%BA%E5%85%B4%E4%B8%BB%E5%9C%BA%E6%9C%8D%E5%8A%A1%E5%B9%B3%E5%8F%B0&tokenUrl=https%3A%2F%2Fzhan.zzxes.com.cn%2F%23%2F (basepage.py:37) PASSED ------ generated html file: file://E:\workspace\ne_p_uitest\report.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`文件 ```python #!/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() ``` 执行该文件: ```shell 测试邮件发送成功! ``` ## 运行 ### 安装依赖 ```shell 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`