selenium是一个针对web端项目的模拟鼠标和键盘操作的自动化测试工具,pytest是一个和unittest类似的自动化测试框架,但它比unittest更加方便,并且可以兼容unittest框架。
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 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服务后的授权码,在qq邮箱-设置-账户中可以开启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() # if __name__ == '__main__': # print(cm.BASE_DIR)
#readconfig.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() # 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 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)) # if __name__ == '__main__': # overview = Element('overview') # print(overview['首页按钮'])
#logger.py import 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) 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() # if __name__=='__main__': # info("======")
#times.py #!/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) # @wraps 不改变使用装饰器原有函数的结构 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"))
#!/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 class WebPage(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.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) #presence_of_element_located((By.ID,"acdid")) 显式等待 def get_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) 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)
账号: 'xpath==//*[@id="login_username"]' 密码: 'xpath==//*[@id="login_password"]' 登录: 'xpath==//*[@id="login"]/div[4]/div/div/div/button'
#!/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_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()
#!/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.yaml 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['登录'])
# -*- 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("测试返工主流程:顺利通过的全套流程") 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("登录后操作") #pytest会自动搜索测试用例,不用在这里调用,这里只是为了单个文件调试的时候使用 # if __name__ == '__main__' : # pytest.main(['test_001_main.py','-s'])#'--capture=no'
#!/usr/bin/env python3 # -*- coding:utf-8 -*- 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] addopts = --html=report.html --self-contained-html
在根目录下,在cmd中直接输入pytest,会自动搜索测试用例,执行完成后在根目录下输出html报告。
#!/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 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,这里在我的另一个文章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') 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()