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中配置文件目录、定位类型及邮箱信息
#!/usr/bin/env python3# -*- coding:utf-8 -*-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
classConfigManager(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':'[email protected]',# 切换成你自己的地址'password':'开启smtp服务后的授权码',#开启smtp服务后的授权码,在qq邮箱-设置-账户中可以开启smtp服务并获取授权码'smtp_host':'smtp.qq.com','smtp_port':465}# 收件人
ADDRESSEE =['[email protected]',]@property#创建只读属性deflog_file(self):"""日志目录"""
log_dir = os.path.join(self.BASE_DIR,'logs')ifnot os.path.exists(log_dir):
os.makedirs(log_dir)return os.path.join(log_dir,'{}.log'.format(dt_strftime()))@propertydefini_file(self):"""配置文件"""
ini_file = os.path.join(self.BASE_DIR,'config','config.ini')ifnot os.path.exists(ini_file):raise FileNotFoundError("配置文件%s不存在!"% ini_file)return ini_file
cm = ConfigManager()# if __name__ == '__main__':# print(cm.BASE_DIR)
二、common中创建readconfig.py和readelement.py
#readconfig.pyimport configparser
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__),'../'))))from config.conf import cm
HOST='HOST'classReadConfig(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)withopen(cm.ini_file,'w')as f:
self.config.write(f)@propertydefurl(self):return self._get(HOST, HOST)
ini = ReadConfig()# if __name__ == '__main__':# print(ini.url)
#readelement.py#!/usr/bin/env python3# -*- coding:utf-8 -*-import os,sys
import yaml
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__),'../'))))from config.conf import cm
classElement(object):"""获取元素"""def__init__(self, name):
self.file_name ='%s.yaml'% name
self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)ifnot os.path.exists(self.element_path):raise FileNotFoundError("%s 文件不存在!"% self.element_path)withopen(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))# if __name__ == '__main__':# overview = Element('overview')# print(overview['首页按钮'])
三、在utils中添加工具类
#logger.pyimport ctypes,sys
STD_INPUT_HANDLE =-10
STD_OUTPUT_HANDLE =-11
STD_ERROR_HANDLE =-12
FOREGROUND_BLACK =0x00# black.
FOREGROUND_DARKBLUE =0x01# dark blue.
FOREGROUND_DARKGREEN =0x02# dark green.
FOREGROUND_DARKSKYBLUE =0x03# dark skyblue.
FOREGROUND_DARKRED =0x04# dark red.
FOREGROUND_DARKPINK =0x05# dark pink.
FOREGROUND_DARKYELLOW =0x06# dark yellow.
FOREGROUND_DARKWHITE =0x07# dark white.
FOREGROUND_DARKGRAY =0x08# dark gray.
FOREGROUND_BLUE =0x09# blue.
FOREGROUND_GREEN =0x0a# green.
FOREGROUND_SKYBLUE =0x0b# skyblue.
FOREGROUND_RED =0x0c# red.
FOREGROUND_PINK =0x0d# pink.
FOREGROUND_YELLOW =0x0e# yellow.
FOREGROUND_WHITE =0x0f# white.
BACKGROUND_BLUE =0x10# dark blue.
BACKGROUND_GREEN =0x20# dark green.
BACKGROUND_DARKSKYBLUE =0x30# dark skyblue.
BACKGROUND_DARKRED =0x40# dark red.
BACKGROUND_DARKPINK =0x50# dark pink.
BACKGROUND_DARKYELLOW =0x60# dark yellow.
BACKGROUND_DARKWHITE =0x70# dark white.
BACKGROUND_DARKGRAY =0x80# dark gray.
BACKGROUND_BLUE =0x90# blue.
BACKGROUND_GREEN =0xa0# green.
BACKGROUND_SKYBLUE =0xb0# skyblue.
BACKGROUND_RED =0xc0# red.
BACKGROUND_PINK =0xd0# pink.
BACKGROUND_YELLOW =0xe0# yellow.
BACKGROUND_WHITE =0xf0# white.
std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)defset_cmd_text_color(color, handle=std_out_handle):
Bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, color)return Bool
defreset():
set_cmd_text_color(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)deferror(mess, end ='\n', flush =True):
set_cmd_text_color(FOREGROUND_RED)print("[ERROR]", mess, end = end, flush = flush)
reset()defwarn(mess, end ='\n', flush =True):
set_cmd_text_color(FOREGROUND_YELLOW)print("[WARN]", mess, end = end, flush = flush)
reset()definfo(mess, end ='\n', flush =True):
set_cmd_text_color(FOREGROUND_GREEN)print("[INFO]", mess, end = end, flush = flush)
reset()defwrite(mess, foregound = FOREGROUND_WHITE, background = FOREGROUND_BLACK, end ='\n', flush =True):
set_cmd_text_color(foregound | background)print(mess, end = end, flush = flush)
reset()# if __name__=='__main__':# info("======")
#times.py#!/usr/bin/env python3# -*- coding:utf-8 -*-import time
import datetime
from functools import wraps
deftimestamp():"""时间戳"""return time.time()defdt_strftime(fmt="%Y%m"):"""
datetime格式化时间
:param fmt "%Y%m%d %H%M%S
"""return datetime.datetime.now().strftime(fmt)defsleep(seconds=1.0):"""
睡眠时间
"""
time.sleep(seconds)defrunning_time(func):"""函数运行时间"""@wraps(func)# @wraps 不改变使用装饰器原有函数的结构defwrapper(*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"))
四、page中创建webpage.py
#!/usr/bin/env python3# -*- coding:utf-8 -*-"""
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
classWebPage(object):"""selenium基类"""def__init__(self, driver):# self.driver = webdriver.Chrome()
self.driver = driver
self.timeout =20
self.wait = WebDriverWait(self.driver, self.timeout)defget_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)@staticmethoddefelement_locator(func, locator):"""元素定位器"""
name, value = locator
return func(cm.LOCATE_MODE[name], value)deffind_element(self, locator):"""寻找单个元素"""return WebPage.element_locator(lambda*args: self.wait.until(
EC.presence_of_element_located(args)), locator)#presence_of_element_located((By.ID,"acdid")) 显式等待defget_attrib(self, locator,value):"""获取元素属性"""
logger.info("获取属性")
ele=self.find_element(locator)
sleep(0.5)return ele.get_attribute(value)# js='document.querySelector("#质检表_返工单号").value'# self.driver.execute_script(js)deffind_elements(self, locator):"""查找多个相同的元素"""return WebPage.element_locator(lambda*args: self.wait.until(
EC.presence_of_all_elements_located(args)), locator)deffind_element_drag(self,locator):
target = self.find_element(locator)
self.driver.execute_script("arguments[0].scrollIntoView();", target)#拖动到可见的元素去defelements_num(self, locator):"""获取相同元素的个数"""
number =len(self.find_elements(locator))
logger.info("相同元素:{}".format((locator, number)))return number
definput_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))definput_enter(self, locator):"""回车、tab等键入"""
ele = self.find_element(locator)
ele.send_keys(Keys.ENTER)defis_click(self, locator):"""点击"""
self.find_element(locator).click()
sleep()
logger.info("点击元素:{}".format(locator))defelement_text(self, locator):"""获取当前的text"""
_text = self.find_element(locator).text
logger.info("获取文本:{}".format(_text))return _text
defhold_on(self,locator):#定位到要悬停的元素
move = self.find_element(locator)#对定位到的元素执行悬停操作
ActionChains(self.driver).move_to_element(move).perform()
sleep()
logger.info("悬停元素:{}".format(locator))defscreen_scoll(self):
self.driver.execute_script('window.scrollBy(0, 300)')
sleep(1)@propertydefget_source(self):"""获取页面源代码"""return self.driver.page_source
defrefresh(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
#!/usr/bin/env python3# -*- coding:utf-8 -*-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_timedefinspect_element():"""检查所有的元素是否正确
只能做一个简单的检查
"""for files in os.listdir(cm.ELEMENT_PATH):
_path = os.path.join(cm.ELEMENT_PATH, files)withopen(_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 notin 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
#!/usr/bin/env python3# -*- coding:utf-8 -*-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')#获取login.yamlclassLoginPage(WebPage):'''登录'''definput_user(self,content):
self.input_text(login['账号'],content)definput_pwd(self,content):
self.input_text(login['密码'],content)defbtn_login(self):
self.is_click(login['登录'])
八、在Test_case中创建测试用例test_001_main.py
# -*- coding:utf-8 -*-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("测试主流程:顺利通过的全套流程")classTestOverview:@allure.step("登录")@pytest.fixture(scope="function")deflogin(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")deftest_001(self, drivers):"""登录后操作"""print("登录后操作")#pytest会自动搜索测试用例,不用在这里调用,这里只是为了单个文件调试的时候使用# if __name__ == '__main__' : # pytest.main(['test_001_main.py','-s'])#'--capture=no'
九、在根目录下添加conftest.py
#!/usr/bin/env python3# -*- coding:utf-8 -*-import pytest
from py.xml import html
from selenium import webdriver
driver [email protected](scope='session', autouse=True)defdrivers(request):global driver
if driver isNone:
driver = webdriver.Chrome()
driver.maximize_window()deffn():
driver.quit()
request.addfinalizer(fn)return driver
@pytest.hookimpl(hookwrapper=True)defpytest_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',[])defpytest_html_results_table_header(cells):
cells.insert(1, html.th('用例名称'))
cells.insert(2, html.th('Test_nodeid'))
cells.pop(2)defpytest_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()
这里要注意创建的drivers函数,因为添加了@pytest.fixture(scope=‘session’, autouse=True)修饰器,这个函数会在session级别的testcase中生效 ,并返回webdriver。在Testoverview类里,每个testcase,如test_001中都传递了一个“drivers”参数,这个drivers就是调用的conftest中的drivers函数。
十、在根目录下新建pytest.ini文件,对pytest执行过程中的操作做全局控制
[pytest]
addopts =--html=report.html --self-contained-html
十一、执行
在根目录下,在cmd中直接输入pytest,会自动搜索测试用例,执行完成后在根目录下输出html报告。
十二、在utils下创建send_mail.py发送邮件
#!/usr/bin/env python3# -*- coding:utf-8 -*-import zmail
import sys,os
sys.path.append((os.path.abspath(os.path.join(os.path.dirname(__file__),'../'))))from config.conf import cm
defsend_report():"""发送报告"""withopen(cm.REPORT_FILE, encoding='utf-8')as f:
content_html = f.read()try:
mail ={'from':'[email protected]','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文件
#!/usr/bin/env python3# -*- coding:utf-8 -*-import sys
import subprocess
WIN = sys.platform.startswith('win')defmain():"""主函数"""
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()
版权归原作者 tianshuiyimo 所有, 如有侵权,请联系我们删除。