selenium是一个针对web端项目的模拟鼠标和键盘操作的自动化测试工具,pytest是一个和unittest类似的自动化测试框架,但它比unittest更加方便,并且可以兼容unittest框架。
项目结构
- common:存放公共方法,比如读取配置文件
- config:存放配置文件。
- logs:存放日志。
- page:对selenium方法进行二次封装。
- page_element:存放页面元素。
- page_object:页面对象设计,将每个页面的功能均封装在这里,然后再testcase中调用,便于维护。
- script:一些额外的脚本文件,我这里放的是检测页面元素格式的文件。
- TestCase:存放测试用例。
- utils:工具类。
- conftest:pytest的配置文件。
- run_case.py:配置生成allure报告的批处理文件,不影响整个测试用例的执行。
一、在config中创建config.ini和conf.py
config.ini写入host信息
[HOST]
HOST=http://rework.dfrobot.work/login
conf.py中配置文件目录、定位类型及邮箱信息
import os,sys
from selenium.webdriver.common.by import By
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
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')
LOCATE_MODE = {
'css': By.CSS_SELECTOR,
'xpath': By.XPATH,
'name': By.NAME,
'id': By.ID,
'class': By.CLASS_NAME
}
EMAIL_INFO = {
'username': 'xxxxxxxx@qq.com',
'password': '开启smtp服务后的授权码',
'smtp_host': 'smtp.qq.com',
'smtp_port': 465
}
ADDRESSEE = [
'xxxxxxxx@qq.com',
]
@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 = os.path.join(self.BASE_DIR, 'config', 'config.ini')
if not os.path.exists(ini_file):
raise FileNotFoundError("配置文件%s不存在!" % ini_file)
return ini_file
cm = ConfigManager()
二、common中创建readconfig.py和readelement.py
import configparser
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from config.conf import cm
HOST='HOST'
class ReadConfig(object):
'''配置文件'''
def __init__(self):
self.config=configparser.RawConfigParser()
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)
@property
def url(self):
return self._get(HOST, HOST)
ini = ReadConfig()
import os,sys
import yaml
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from config.conf import cm
class Element(object):
"""获取元素"""
def __init__(self, name):
self.file_name = '%s.yaml' % name
self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)
if not os.path.exists(self.element_path):
raise FileNotFoundError("%s 文件不存在!" % self.element_path)
with open(self.element_path, encoding='utf-8') as f:
self.data = yaml.safe_load(f)
def __getitem__(self, item):
"""获取属性"""
data = self.data.get(item)
if data:
name, value = data.split('==')
return name, value
raise ArithmeticError("{}中不存在关键字:{}".format(self.file_name, item))
三、在utils中添加工具类
import ctypes,sys
STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12
FOREGROUND_BLACK = 0x00
FOREGROUND_DARKBLUE = 0x01
FOREGROUND_DARKGREEN = 0x02
FOREGROUND_DARKSKYBLUE = 0x03
FOREGROUND_DARKRED = 0x04
FOREGROUND_DARKPINK = 0x05
FOREGROUND_DARKYELLOW = 0x06
FOREGROUND_DARKWHITE = 0x07
FOREGROUND_DARKGRAY = 0x08
FOREGROUND_BLUE = 0x09
FOREGROUND_GREEN = 0x0a
FOREGROUND_SKYBLUE = 0x0b
FOREGROUND_RED = 0x0c
FOREGROUND_PINK = 0x0d
FOREGROUND_YELLOW = 0x0e
FOREGROUND_WHITE = 0x0f
BACKGROUND_BLUE = 0x10
BACKGROUND_GREEN = 0x20
BACKGROUND_DARKSKYBLUE = 0x30
BACKGROUND_DARKRED = 0x40
BACKGROUND_DARKPINK = 0x50
BACKGROUND_DARKYELLOW = 0x60
BACKGROUND_DARKWHITE = 0x70
BACKGROUND_DARKGRAY = 0x80
BACKGROUND_BLUE = 0x90
BACKGROUND_GREEN = 0xa0
BACKGROUND_SKYBLUE = 0xb0
BACKGROUND_RED = 0xc0
BACKGROUND_PINK = 0xd0
BACKGROUND_YELLOW = 0xe0
BACKGROUND_WHITE = 0xf0
std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
def set_cmd_text_color(color, handle=std_out_handle):
Bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, color)
return Bool
def reset():
set_cmd_text_color(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
def error(mess, end = '\n', flush = True):
set_cmd_text_color(FOREGROUND_RED)
print("[ERROR]", mess, end = end, flush = flush)
reset()
def warn(mess, end = '\n', flush = True):
set_cmd_text_color(FOREGROUND_YELLOW)
print("[WARN]", mess, end = end, flush = flush)
reset()
def info(mess, end = '\n', flush = True):
set_cmd_text_color(FOREGROUND_GREEN)
print("[INFO]", mess, end = end, flush = flush)
reset()
def write(mess, foregound = FOREGROUND_WHITE, background = FOREGROUND_BLACK, end = '\n', flush = True):
set_cmd_text_color(foregound | background)
print(mess, end = end, flush = flush)
reset()
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
四、page中创建webpage.py
"""
selenium基类
本文件存放了selenium基类的封装方法
"""
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from config.conf import cm
from utils.times import sleep
from utils import logger
class WebPage(object):
"""selenium基类"""
def __init__(self, driver):
self.driver = driver
self.timeout = 20
self.wait = WebDriverWait(self.driver, self.timeout)
def get_url(self, url):
"""打开网址并验证"""
self.driver.set_page_load_timeout(60)
try:
self.driver.get(url)
self.driver.implicitly_wait(10)
logger.info("打开网页:%s" % url)
except TimeoutException:
raise TimeoutException("打开%s超时请检查网络或网址服务器" % url)
@staticmethod
def element_locator(func, locator):
"""元素定位器"""
name, value = locator
return func(cm.LOCATE_MODE[name], value)
def find_element(self, locator):
"""寻找单个元素"""
return WebPage.element_locator(lambda *args: self.wait.until(
EC.presence_of_element_located(args)), locator)
def get_attrib(self, locator,value):
"""获取元素属性"""
logger.info("获取属性")
ele=self.find_element(locator)
sleep(0.5)
return ele.get_attribute(value)
def find_elements(self, locator):
"""查找多个相同的元素"""
return WebPage.element_locator(lambda *args: self.wait.until(
EC.presence_of_all_elements_located(args)), locator)
def find_element_drag(self,locator):
target = self.find_element(locator)
self.driver.execute_script("arguments[0].scrollIntoView();", target)
def elements_num(self, locator):
"""获取相同元素的个数"""
number = len(self.find_elements(locator))
logger.info("相同元素:{}".format((locator, number)))
return number
def input_text(self, locator, txt):
"""输入(输入前先清空)"""
sleep(0.5)
ele = self.find_element(locator)
ele.clear()
ele.send_keys(txt)
sleep(0.5)
logger.info("输入文本:{}".format(txt))
def input_enter(self, locator):
"""回车、tab等键入"""
ele = self.find_element(locator)
ele.send_keys(Keys.ENTER)
def is_click(self, locator):
"""点击"""
self.find_element(locator).click()
sleep()
logger.info("点击元素:{}".format(locator))
def element_text(self, locator):
"""获取当前的text"""
_text = self.find_element(locator).text
logger.info("获取文本:{}".format(_text))
return _text
def hold_on(self,locator):
move = self.find_element(locator)
ActionChains(self.driver).move_to_element(move).perform()
sleep()
logger.info("悬停元素:{}".format(locator))
def screen_scoll(self):
self.driver.execute_script('window.scrollBy(0, 300)')
sleep(1)
@property
def get_source(self):
"""获取页面源代码"""
return self.driver.page_source
def refresh(self):
"""刷新页面F5"""
self.driver.refresh()
self.driver.implicitly_wait(30)
五、page_element下创建login.yaml文件,记录元素位置
账号: 'xpath==//*[@id="login_username"]'
密码: 'xpath==//*[@id="login_password"]'
登录: 'xpath==//*[@id="login"]/div[4]/div/div/div/button'
六、在script下创建inspect.py
import os,sys
import yaml
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from config.conf import cm
from utils.times import running_time
@running_time
def inspect_element():
"""检查所有的元素是否正确
只能做一个简单的检查
"""
for files in os.listdir(cm.ELEMENT_PATH):
_path = os.path.join(cm.ELEMENT_PATH, files)
with open(_path, encoding='utf-8') as f:
data = yaml.safe_load(f)
for k in data.values():
try:
pattern, value = k.split('==')
except ValueError:
raise Exception("{} : {} 元素表达式中没有`==`".format(_path,k))
if pattern not in cm.LOCATE_MODE:
raise Exception('%s中元素【%s】没有指定类型' % (_path, k)
else:
assert value, '%s中元素【%s】类型与值不匹配' % (_path, k)
if __name__ == '__main__':
inspect_element()
七、在page_object下创建login.py
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from page.webpage import WebPage
from common.readelement import Element
login = Element('login')
class LoginPage(WebPage):
'''登录'''
def input_user(self,content):
self.input_text(login['账号'],content)
def input_pwd(self,content):
self.input_text(login['密码'],content)
def btn_login(self):
self.is_click(login['登录'])
八、在Test_case中创建测试用例test_001_main.py
import re,os,sys
import allure
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
from utils.times import sleep
import pytest
from pytest import assume
from utils import logger
from utils.times import sleep,dt_strftime
from common.readconfig import ini
from page_object.login import LoginPage
@allure.story("测试主流程:顺利通过的全套流程")
class TestOverview:
@allure.step("登录")
@pytest.fixture(scope="function")
def login(self, drivers):
"""登录"""
login = LoginPage(drivers)
login.get_url(ini.url)
login.input_user('xxxxx')
login.input_pwd('xxxxxx')
login.btn_login()
@allure.step("登录后的操作")
@pytest.mark.usefixtures("login")
def test_001(self, drivers):
"""登录后操作"""
print("登录后操作")
九、在根目录下添加conftest.py
import pytest
from py.xml import html
from selenium import webdriver
driver = None
@pytest.fixture(scope='session', autouse=True)
def drivers(request):
global driver
if driver is None:
driver = webdriver.Chrome()
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', [])
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_html(report, data):
if report.passed:
del data[:]
data.append(html.div('通过的用例未捕获日志输出.', class_='empty log'))
def _capture_screenshot():
'''
截图保存为base64
:return:
'''
return driver.get_screenshot_as_base64()
十、在根目录下新建pytest.ini文件,对pytest执行过程中的操作做全局控制
[pytest]
addopts = --html=report.html --self-contained-html
十一、执行
在根目录下,在cmd中直接输入pytest,会自动搜索测试用例,执行完成后在根目录下输出html报告。
十二、在utils下创建send_mail.py发送邮件
import zmail
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))))
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': 'xxxxxxxxxx@qq.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(e))
if __name__ == "__main__":
'''请先在config/conf.py文件设置QQ邮箱的账号和密码'''
send_report()
十二、生成allure报告
需要先安装allure,这里在我的另一个文章python3+unittest+selenium自动化实战 中有详细介绍,但是在那篇文章里,使用了命令行的方式来打开allure server,需要输入多次命令。这里为了简化操作,将所有命令写入一个py文件中,我们只需要运行这个py文件,就可以执行测试用例,并且自动打开生成的allure报告。 因此,在根目录下创建一个run_case.py文件
import sys
import subprocess
WIN = sys.platform.startswith('win')
def main():
"""主函数"""
steps = [
"venv\\Script\\activate" if WIN else "source venv/bin/activate",
"pytest --alluredir allure-results --clean-alluredir",
"allure generate allure-results -c -o allure-report",
"allure open allure-report"
]
for step in steps:
subprocess.run("call " + step if WIN else step, shell=True)
if __name__ == "__main__":
main()
|