1、目录结构如下
2、main.py
import os
import shutilfrom playwright.sync_api import sync_playwright
from config.setting import config
from utils.template import Template
from utils.md5 import Md5
from utils.delete import del_files
import pytest
from utils.dir_check import check_dir
from utils.baseurl import get_baseUrldef run():check_dir()data = os.listdir('data')m = Md5('case', 'log', 'case_md5.json')n = Md5('utils', 'log', 'template_md5.json')filter_list = m.filter()utils_list = n.filter()if 'template.py' not in utils_list:filter_list = []n.write_md5()for i in data:file_path = 'data' + '/' + iif os.path.isfile(file_path):temp = 'test_' + iif temp not in filter_list:Template.create_test_file(file_path, 'case')m.write_md5()if __name__ == "__main__":run()del_files('results')pytest.main(['-s', '--alluredir=results'])os.system('allure generate --clean ./results/ -o ./report/')for file_name in os.listdir('resource'):src_file = os.path.join('resource', file_name)dst_file = os.path.join('report', file_name)if os.path.exists(dst_file):os.remove(dst_file)shutil.copy(src_file, 'report')os.system('allure open -h 127.0.0.1 -p 8883 ./report/')
3、conftest.py
import pytest
from playwright.sync_api import sync_playwright
from config.setting import config
from playwright.sync_api import Page
from utils.operate import operate
from utils.baseurl import get_baseUrl
import os
import allure
from utils.video import generate_video@pytest.fixture(scope='session')
def page():browser = sync_playwright().start().chromium.launch(headless=False, slow_mo=500)page = browser.new_page(ignore_https_errors=True, record_video_dir='temp')page.goto(get_baseUrl(config))operate(config['username'], page)operate(config['password'], page)operate(config['submit'], page)return pagedef log(request):with open('log/http.txt', 'a', encoding='utf-8') as w:w.write(f'{request}.url' + '\n')@pytest.fixture(scope='function', autouse=True)
def after(page: Page):yieldpage.on("request", lambda request: log(request))@pytest.fixture(scope='session', autouse=True)
def clear(page: Page):yield# page.close()p = generate_video('temp', 'video')allure.attach.file(p, f'{os.path.basename(p)}', attachment_type=allure.attachment_type.WEBM, extension='WEBM')
4、case目录,内容和目录都是自动生成
5、config目录,保存配置
dir_collection.py
配置中的目录都是自动生成
dir_collections = ['case','log','img','video','temp'
]
env.py
环境变量配置
env = {'prod': '','dev': '','test': 'http://test.lan'
}
setting.py
config = {'baseUrl': '','url': '/user/login','username': {'selector': '#userName','type': 'input','value': 'test'},'password': {'selector': '#password','type': 'input','value': '123'},'submit': {'selector': '#root > div > div > div:nth-child(1) > div > form > div:nth-child(3) > button','type': 'button'},
}
6、data目录
case中的测试文件,便是依据data中的数据自动生成的
homepage.py
homepage_cfg = [{'name': 'homepage','url': '','step': [],'assert': [{'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(1) > div ''> div > div._3A9TZ-vnPrcf2IwqBmUPoX','value': '违规告警数量'},{'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(2) > div ''> div > div._3A9TZ-vnPrcf2IwqBmUPoX','value': '确认违规告警数量1'},{'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(3) > div ''> div > div._3A9TZ-vnPrcf2IwqBmUPoX','value': '未确认违规告警数量'},]}]
keyword.py
keyword_cfg = [{'name': 'keyword','url': '/keyword/info','step': [{"type": 'input',"selector": 'text=关键词组名称',"value": 'UI测试'},{"type": 'input',"selector": 'text=关键词组描述',"value": 'UI新建关键词'},],'assert': [{'selector': '#content > div > div > div > h3','value': '新增关键词策略'},{'selector': '#content > div > div > div > div > div > div > div > form > div:nth-child(1) > div.ant-form-item-label > label','value': '关键词组名称'},{'selector': '#content > div > div > div > div > div > div > div > form > div:nth-child(2) > div.ant-form-item-label > label','value': '关键词组描述'},]}]
7、img目录,保存错误截图的目录,自动生成
8、log目录,保存请求日志和两个md5文件,这两个md5文件主要用来辨别每次运行是否要重新生成case目录中的测试文件
9、report目录,allure命令自动生成
10、resource目录,由于对allure的报告进行了小量的修改,所以,需要保留resource目录,当生成report后,就会将resource目录中的内容和report中的内容替换
11、results目录,allure命令生成,保存测试结果数据
12、temp目录,自动生成临时目录,录制的视频文件会存到temp,然后会对视频进行改名另存到video目录,temp每次运行前自动生成,运行后,自动删除
13、utils目录,存储封装方法的目录
add_style.py
from playwright.sync_api import Pagedef add_style(page: Page, elements, flag: int):if flag == 0:script = f"document.querySelector('{elements}').setAttribute('style','border-style:solid " \f";border-color:green') "else:script = f"document.querySelector('{elements}').setAttribute('style','border-style:solid " \f";border-color:red') "page.evaluate(script)
assert_element.py
from typing import List
from playwright.sync_api import Page
from utils.add_style import add_style
from utils.screenshot import error_screenshotdef assert_element(arr: List, page: Page):li = []if arr:for i in arr:if page.query_selector(i['selector']):text = page.query_selector(i['selector']).inner_text()if text == i['value']:add_style(page, i['selector'], 0)passelse:add_style(page, i['selector'], 1)li.append(i['value'])else:li.append(i['selector'])if li:error_screenshot(page, 'img')raise AssertionError(f"some elements in {str(li)} isn't matched or exists")
baseurl.py
from typing import Dict
from config.env import env
from utils.params_error import ParamsErrordef get_baseUrl(conf: Dict):import sysif len(sys.argv) > 1:if sys.argv[1] == 'dev':conf['baseUrl'] = env['dev']elif sys.argv[1] == 'prod':conf['baseUrl'] = env['prod']elif sys.argv[1] == 'test':conf['baseUrl'] = env['test']else:raise ParamsError('python main.py [test]|[prod]|[dev]')else:raise ParamsError('python main.py [test]|[prod]|[dev]')url = conf['baseUrl'] + conf['url']return url
delete.py
import osdef del_files(dir_path: str):if os.path.exists(dir_path):for filename in os.listdir(dir_path):filepath = os.path.join(dir_path, filename)try:if os.path.isfile(filepath):os.unlink(filepath)except Exception as e:print(f"Error deleting {filepath}: {e}")
dir_check.py
import os
from config.dir_collection import dir_collectionsdef check_dir():li = os.listdir()for i in dir_collections:if i not in li:os.mkdir(i)
md5.py
import hashlib
import json
import os
from json import JSONDecodeErrorclass Md5:def __init__(self, dir_path, md5_path, file_name):self.dir_path = dir_path # 目录路径self.md5_path = md5_path # MD5文件路径self.file_name = file_name # MD5文件名file_path = os.path.join(md5_path, file_name)if not os.path.exists(file_path):open(file_path, mode='w+', encoding='utf-8').close() # 如果MD5文件不存在,则创建该文件def generate_md5(self):temp = {}# 如果dir_path是文件而不是目录,则抛出IOError异常if os.path.isfile(self.dir_path):raise IOError(f'Message: parameter <dir_path:{self.dir_path}> must be directory')else:dir_list = os.listdir(self.dir_path) # 获取目录下的文件列表if len(dir_list) != 0:for i in dir_list:md5 = hashlib.md5() # 创建MD5对象file_path = os.path.join(self.dir_path, i) # 获取文件路径if os.path.isfile(file_path) and os.path.basename(file_path).endswith('.py'): # 如果是文件with open(file_path, mode='r', encoding='utf-8') as f:md5.update(f.read().encode(encoding='utf-8')) # 更新MD5值hex_md5 = md5.hexdigest() # 获取MD5值temp[i] = hex_md5 # 将文件名和MD5值添加到字典中return temp # 返回字典def write_md5(self):file_path = os.path.join(self.md5_path, self.file_name)# 将generate_md5()生成的字典写入到文件中json.dump(self.generate_md5(), open(file_path, mode='w+', encoding='utf-8'))def read_md5(self):file_path = os.path.join(self.md5_path, self.file_name)try:with open(file_path, mode='r', encoding='utf-8') as f:# 读取文件中的json数据并返回return json.load(f)except JSONDecodeError:# 如果文件中的json数据解析失败,则返回空字典return {}def filter(self):old_md5 = self.read_md5() # 获取旧的MD5值new_md5 = self.generate_md5() # 获取新的MD5值# 返回新旧md5值相同的文件名列表return [k for k, v in new_md5.items() if k in old_md5 and v == old_md5[k]]
operate.py
from playwright.sync_api import Pagedef operate(d: dict, page: Page):if d.get('type') == 'input':page.query_selector(d.get('selector')).fill(d.get('value'))elif d.get('type') == 'button':page.query_selector(d.get('selector')).click()
params_error.py
class ParamsError(Exception):def __init__(self, msg: str):super(ParamsError, self).__init__(msg)
parse.py
from playwright.sync_api import Page
from config.setting import configdef parse(conf: dict, page: Page):url = config['baseUrl'] + conf['url']if url != '':page.goto(url)if conf['step']:for i in conf['step']:if i.get('type') == 'input':page.query_selector(i.get('selector')).fill(i.get('value'))elif i.get('type') == 'button':page.query_selector(i.get('selector')).click()
screenshot.py
import timeimport allure
from playwright.sync_api import Pagedef error_screenshot(page: Page, path: str):file_path = f'{path}/{int(time.time())}.png'page.screenshot(path=file_path, type='png', full_page=True)allure.attach.file(file_path, f'{path}/{int(time.time())}', attachment_type=allure.attachment_type.PNG,extension='PNG')
template.py
import osclass Template:@staticmethoddef check_todo_file(file_path: str) -> bool:"""检查文件内容中是否包含 '# TODO' 字符串Args:file_path (str): 文件路径Returns:bool: 如果包含 '# TODO' 字符串则返回 True,否则返回 False"""with open(file_path, mode='r+', encoding='utf-8') as file:return '# TODO' in file.read()@staticmethoddef create_test_file(file_path: str, target_path: str) -> None:"""创建测试文件Args:file_path (str): 文件路径target_path (str): 目标路径"""if Template.check_todo_file(file_path):print(f'Message: 发现 <TODO> 标记,文件 <{file_path}> 尚未完成')returnfile_name = os.path.basename(file_path).replace('.py', '')import_name = f'{file_name}_cfg'test_file_path = os.path.join(target_path, f'test_{file_name}.py')with open(test_file_path, mode='w+', encoding='utf-8') as file:file.write(f'''import pytest
import allure
from data.{file_name} import {import_name}
from playwright.sync_api import Page
from utils.parse import parse
from utils.assert_element import assert_element@allure.suite('{file_name}')
class Test_{file_name.capitalize()}:@allure.sub_suite('{import_name}')@pytest.mark.parametrize('cfg', {import_name})def test_{file_name}(self, cfg, page): parse(cfg, page)allure.dynamic.title(cfg['name'])assert_element(cfg['assert'], page)
''')print(f"Message: 文件 <{test_file_path}> 创建成功")
video.py
import os
import time# def remove_video(path: str):
# print(os.listdir(path))
# if os.listdir(path):
# for i in os.listdir(path):
# os.remove(f'{path}/{i}')def generate_video(source_path: str, target_path: str):p = f"{target_path}/{int(time.time())}.webm"while True:if os.listdir(source_path):for i in os.listdir(source_path):os.renames(f'{source_path}/{i}', p)breakreturn p
14、video目录自动生成,存放录制视频的目录
15、报告效果