1、目录结构如下
2、main.py
import os
import shutil
from 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_baseUrl
def 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' + '/' + i
if os.path.isfile(file_path):
temp = 'test_' + i
if 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 page
def 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):
yield
page.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 Page
def 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_screenshot
def 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)
pass
else:
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 ParamsError
def get_baseUrl(conf: Dict):
import sys
if 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 os
def 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_collections
def 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 JSONDecodeError
class 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 Page
def 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 config
def 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 time
import allure
from playwright.sync_api import Page
def 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 os
class Template:
@staticmethod
def 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()
@staticmethod
def 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}> 尚未完成')
return
file_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)
break
return p
14、video目录自动生成,存放录制视频的目录
15、报告效果