一文学懂 python3+selenium
一、目录结构及架构
- common:存放公共使用库
- config:配置目录
- data:测试数据,yaml格式
- driver:存放浏览器驱动的exe
- elements:测试页面元素,yaml格式
- img:存放执行用例时截图后图片的路径
- logs:日志路径
- pages:每个页面py文件,包括basepage
- reports:执行用例后生成报告路径
- testcases:存放每个用例
- venv:环境
- run_testcase.py:程序入口
为了降低web自动化测试的维护成本,降低代码冗余,提升测试用例的可读性,故衍生出了PageObject模式。
PO模式主要体现在对界面交互细节的封装,大体分为如下三层:
1、BasePage层:封装页面操作最基本的方法,如:打开/关闭浏览器,定位元素等,可被其他对象继承
2、Page层:提供对具体页面元素的定位、操作方法的封装
3、testcase业务层:传入具体的参数,组织业务流程,执行自动化
二、目录及代码介绍
-
common
代码:
#!/usr/bin/env python # -*- coding: UTF-8 -*- import configparser import os import sys from selenium import webdriver from common.global_variable import Common from time import sleep from common.logger import Logger log = Logger() class BrowserEngine(object): # 配置文件路径 config_path = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), 'config', 'config.ini') # chromedriver 路径 chrome_driver_path = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), 'driver', 'chromedriver.exe') # msedgedriver 路径 edge_driver_path = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), 'driver', 'msedgedriver.exe') def open_browser(self): """ 打开浏览器 :param driver: :return: """ # 读取配置文件,获取浏览器驱动类型、目标 url config = configparser.ConfigParser() config.read(self.config_path) browser = config.get("browserType", "browserName") url = config.get("baseUrl", "url") # 判断配置文件读取到的浏览器驱动类型 if browser == "chrome": self.driver = webdriver.Chrome(self.chrome_driver_path) elif browser == "edge": self.driver = webdriver.Edge(self.edge_driver_path) self.driver.get(url) self.driver.maximize_window() log.info("打开浏览器, browser is [%s], url is [%s]" % (browser, url)) Common().set_driver(self.driver) def close_browser(self): """ 关闭浏览器 :return: """ log.info("关闭浏览器") Common.driver.quit()
import os import yaml class GetYamlUtils(object): def __init__(self, page_name): """ 拼接页面地址,如果不存在则自动创建 """ self.page_name = page_name self.cur_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) def get_elements(self): """ 获取页面元素 """ self.page_path = os.path.join(os.path.join(self.cur_path, 'elements'), self.page_name) if not os.path.exists(self.page_path): os.mkdir(self.page_path) with open(self.page_path, 'r', encoding='utf-8') as f: elements = yaml.load(f, Loader=yaml.FullLoader) return elements def get_data(self): """ 获取数据 """ self.page_path = os.path.join(os.path.join(self.cur_path, 'data'), self.page_name) if not os.path.exists(self.page_path): os.mkdir(self.page_path) with open(self.page_path, 'r', encoding='utf-8') as f: data = yaml.load(f, Loader=yaml.FullLoader) return data
#!/usr/bin/env python # -*- coding: UTF-8 -*- class Common: driver = None # 类变量driver,全局其他地方通过这个类变量来访问driver对象 def set_driver(self, driver): Common.driver = driver return Common.driver
import logging import time import os import sys class Logger: def __init__(self): # 设置日志存储路径/日志名称 root_path = os.path.dirname(os.path.abspath(sys.argv[0])) current_time = time.strftime('%Y-%m-%d-%H%M%S') self.log_name = os.path.join(root_path, 'logs', f'faxservertest-{current_time}.log') def printConsoleLog(self, level, message): # 创建一个 logger logger = logging.getLogger() logger.setLevel(logging.DEBUG) # 创建一个 handler,用于写入日志文件 file_handler = logging.FileHandler(self.log_name, 'a', encoding='utf-8') file_handler.setLevel(logging.DEBUG) # 再创建一个 handler,用于输出到控制台 console_handler = logging.StreamHandler() console_handler.setLevel(logging.DEBUG) # 定义 handler 的输出格式 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # 给logger添加handler logger.addHandler(file_handler) logger.addHandler(console_handler) # 记录一条日志 if level == 'info': logger.info(message) elif level == 'debug': logger.debug(message) elif level == 'warning': logger.warning(message) elif level == 'error': logger.error(message) logger.removeHandler(file_handler) logger.removeHandler(console_handler) # 关闭打开的文件 file_handler.close() def debug(self, message): self.printConsoleLog('debug', message) def info(self, message): self.printConsoleLog('info', message) def warning(self, message): self.printConsoleLog('warning', message) def error(self, message): self.printConsoleLog('error', message) if __name__ == '__main__': pass
import unittest import os import sys from common.logger import Logger from common.global_variable import Common log = Logger() class UnittestCommon(unittest.TestCase): def setUp(self): """ 执行测试用例前的操作 :return: """ log.info("==================== Start Testing ====================") @classmethod def setUpClass(cls): pass # log.info("==================== Start Testing ====================") @classmethod def tearDownClass(cls): pass # log.info("==================== End Testing ====================") def tearDown(self): """ 执行完测试用例后的操作 :return: """ log.info("==================== End Testing ====================") def save_img(self, img_name): root_path = os.path.dirname(os.path.abspath(sys.argv[0])) screenshots_name = os.path.join(root_path, 'img', f'{img_name}.png') if os.path.exists(screenshots_name): os.remove(screenshots_name) try: Common.driver.get_screenshot_as_file(screenshots_name) except NameError as e: log.error("failed to get windows img with %s" % e) if __name__ == '__main__': pass
-
config
配置:
[browserType] browserName = chrome [baseUrl] url = http://193.1.1.121/webserver/
-
data
例如:
#用户名 username: test #密码 password: 123456
-
driver
-
elements
例如:
#提示界面,如果有多个,取最后一个 #header header: xpath=>//div[@class='x-window x-message-box x-layer x-window-default x-closable x-window-closable x-window-default-closable x-border-box'][last()]/div[contains(@id,'header')] #body body: xpath=>//div[@class='x-window x-message-box x-layer x-window-default x-closable x-window-closable x-window-default-closable x-border-box'][last()]/div[contains(@id,'body')] #toolbar toolbar: xpath=>//div[@class='x-window x-message-box x-layer x-window-default x-closable x-window-closable x-window-default-closable x-border-box'][last()]/div[contains(@id,'toolbar')] #显示内容:display span_text_mode: //td/div[contains(@id,'displayfield-input')]/span #按钮:button span_button_mode: //a/span/span/span[text()='{}']/..
-
pages
base_page.py
#!/usr/bin/env python # -*- coding: UTF-8 -*- """ @File :base_page.py @Desc :页面基类,封装一些浏览器操作常用方法 """ import os import sys import time from selenium.common.exceptions import NoSuchElementException,TimeoutException,WebDriverException from common.global_variable import Common from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys from common.browser_engine import BrowserEngine from common.logger import Logger log = Logger() class BasePage(object): """ 封装页面通用方法,比如:driver的实例化 """ def __init__(self, base_driver=None): if base_driver is not None: self.driver = base_driver else: self.driver = Common.driver #如果 WebDriver没有在 DOM中找到元素,将继续等待,超出设定时间后则抛出找不到元素的异常,换句话说,当查找元素或元素并没有立即出现的时候,隐式等待将等待一段时间再查找 DOM,默认的时间是0 self.driver.implicitly_wait(30) #隐式等待20秒,webdriver驱动器对象下的方法;针对是一次会话所有操作,相当于是一个全局等待;只需要声明定义一次,通常在设计脚本中会应用于setUp方法; def close_browser(self): """ 退出浏览器 :return: """ try: self.driver.quit() except NameError as e: log.error("failed to close browser with %s" % e) def forward(self): """ 浏览器前进操作 :return: """ try: self.driver.forward() except NameError as e: log.error("failed to forward browser with %s" % e) def back(self): """ 浏览器返回操作 :return: """ try: self.driver.back() except NameError as e: log.error("failed to back browser with %s" % e) def close_current_window(self): """ 关闭当前窗口 :return: """ try: self.driver.close() except NameError as e: log.error("failed to close the current window with %s" % e) def find_element(self, selector): """ 元素定位 :param selector: :return: """ element = '' if '=>' not in selector: return self.driver.find_element_by_id(selector) selector_by = selector.split('=>')[0] selector_value = selector.split('=>')[1] if selector_by == 'id': try: element=WebDriverWait(self.driver,20).until(EC.presence_of_element_located((By.ID,selector_value))) #element = self.driver.find_element_by_id(selector_value) log.info("by is [%s], value is [%s]" % (selector_by, selector_value)) except NoSuchElementException as e: log.error("failed to find element with %s" % e) except TimeoutException as e: log.error("find element timeout: %s" % e) elif selector_by == 'xpath': try: element = WebDriverWait(self.driver, 20).until(EC.presence_of_element_located((By.XPATH, selector_value))) #element = self.driver.find_element_by_xpath(selector_value) log.info("by is [%s], value is [%s]" % (selector_by, selector_value)) except NoSuchElementException as e: log.error("failed to find element with %s" % e) except TimeoutException as e: log.error("find element timeout: %s" % e) else: raise NameError("please enter a valid type of targeting elements.") return element def find_elements(self, selector): """ 元素定位 :param selector: :return: """ elements = '' if '=>' not in selector: return self.driver.find_elements_by_id(selector) selector_by = selector.split('=>')[0] selector_value = selector.split('=>')[1] if selector_by == 'id': try: elements = WebDriverWait(self.driver, 20).until(EC.presence_of_all_elements_located((By.ID, selector_value))) #elements = self.driver.find_elements_by_id(selector_value) log.info("by is [%s], value is [%s]" % (selector_by, selector_value)) except NoSuchElementException as e: log.error("failed to find element with %s" % e) except TimeoutException as e: log.error("find element timeout: %s" % e) elif selector_by == 'xpath': try: elements = WebDriverWait(self.driver, 20).until( EC.presence_of_all_elements_located((By.XPATH, selector_value))) #elements = self.driver.find_elements_by_xpath(selector_value) log.info("by is [%s], value is [%s]" % (selector_by, selector_value)) except NoSuchElementException as e: log.error("failed to find element with %s" % e) except TimeoutException as e: log.error("find element timeout: %s" % e) else: raise NameError("please enter a valid type of targeting elements.") return elements def input(self, selector, text, s=False): """ 文本输入 :param selector: :param text: :return: """ if s: eles = self.find_elements(selector) try: for ele in eles: if ele.is_displayed(): ele.clear() ele.send_keys(text) self.sleep(0.5) log.info("send keys is [%s]" % text) except NameError as e: log.error("failed to entered in the input box with %s" % e) else: ele = self.find_element(selector) ele.clear() try: ele.send_keys(text) self.sleep(0.5) log.info("send keys is [%s]" % text) except NameError as e: log.error("failed to entered in the input box with %s" % e) def clear(self, selector): """ 清除文本框内容 :param selector: :return: """ ele = self.find_element(selector) try: ele.clear() except NameError as e: log.error("failed to clear the input box content with %s" % e) def click(self, selector, s=False): """ 点击元素事件 :param selector: :return: """ # log.info("---click is in: " + selector) if s: eles=self.find_elements(selector) try: for ele in eles: if ele.is_displayed(): #self.sleep(0.5) ele.click() except NameError as e: log.error("failed to click the element with %s" % e) else: ele = self.find_element(selector) try: #self.sleep(0.5) ele.click() except NameError as e: log.error("failed to click the element with %s" % e) def get_page_title(self): """ 获取网页标题 :return: """ log.info("Current page title is [%s]" % self.driver.title) return self.driver.title def get_page_text(self, selector): """ 获取span标签的内容 :return: """ ele = self.find_element(selector) text='' try: log.info("text is [%s]" % ele.text) text=ele.text except NameError as e: log.error("failed to find span text %s" % e) finally: return text def get_attribute(self,selector,keyword): """ 获取标签某一属性 :return: """ ele = self.find_element(selector) value = '' try: value=ele.get_attribute(keyword) if keyword is "innerHTML": log.info("keyword id [%s];value is [html...]" % (keyword)) else: log.info("keyword id [%s];value is [%s]" % (keyword,value)) except NameError as e: log.error("failed to find span text %s" % e) finally: return value def switch_to_alert(self, accept=True): try: alert=self.driver.switch_to_alert() log.info("alert text is : [%s]" % (alert.text)) if accept: alert.accept() else: alert.dismiss() except Exception as e: log.error("not find alert %s" % e) def get_all_handles(self): all_handles = self.driver.window_handles return all_handles #将某个元素拖拽到某个元素然后松开 ----> drag_and_drop(source, target) def select_move_drop(self, source_selector, target_selector): try: dragElement=self.find_element(source_selector) targetElement=self.find_element(target_selector) Action=ActionChains(self.driver) Action.drag_and_drop(dragElement,targetElement).perform() self.sleep(5) except Exception as e: log.error("failed to drag and drop %s" % e) def move_to_element_xy(self,to_element,x,y): e=self.find_element(to_element) ActionChains(self.driver).move_to_element_with_offset(e,x,y).click().perform() #某个键盘键被按下 def key_down(self,value): ActionChains(self.driver).key_down(value=value).perform() #松开某个键 def key_up(self,value): ActionChains(self.driver).key_up(value=value).perform() #鼠标右键单击 def right_click(self): ActionChains(self.driver).context_click().perform() #鼠标左键双击 def double_click(self): ActionChains(self.driver).double_click().perform() @staticmethod def sleep(seconds): """ 等待 :param seconds: :return: """ time.sleep(seconds)
例如:login_page.py
#!/usr/bin/env python # -*- coding: UTF-8 -*- from pages.base_page import BasePage from common.get_yaml_utils import GetYamlUtils from common.logger import Logger log = Logger() """ 在这个页面登录界面,例如登录,忘记密码等 """ class LoginPage(BasePage): login_ele = GetYamlUtils('login_page.yaml').get_elements() username_input = (login_ele['username_input']) password_input = (login_ele['password_input']) login_button = (login_ele['login_button']) login_success=(login_ele['login_success']) login_data = GetYamlUtils('login_page.yaml').get_data() username= (login_data['username']) password= (login_data['password']) def __init__(self): super(LoginPage, self).__init__() log.info("----------进入登录界面----------") def input_username(self, username=None): if username is None: username = self.username log.info("----------输入用户名: {}----------".format(username)) self.input(self.username_input, username) def input_password(self, password=None): if password is None: password = self.password log.info("----------输入密码: {}----------".format(password)) self.input(self.password_input, password) def click_login_btn(self): log.info("----------点击 登录 按钮----------") self.click(self.login_button) def check_login_success(self): log.info("----------检查登录是否成功?----------") return self.get_page_text(self.login_success)
-
testcases
例如:test_a_login_page.py
#!/usr/bin/env python # -*- coding: UTF-8 -*- import time import os import sys from pages.login_page import LoginPage from common.logger import Logger from common.unittest_common import UnittestCommon from BeautifulReport import BeautifulReport from pages.tip_page import TipPage log = Logger() class TestLoginPage(UnittestCommon): @BeautifulReport.add_test_img('用户名不存在-登录失败') def test_a_login_fail(self): """ 登录-用户名不存在,登录失败 """ try: login = LoginPage() #输入用户名,用户名不存在 login.input_username(username='zhangsan') #输入密码 login.input_password() #点击登录按钮 login.click_login_btn() time.sleep(2) self.save_img(img_name='用户名不存在-登录失败') #关闭提示 tip=TipPage() tip.click_btn(name='确定') time.sleep(5) log.info('Test pass') except Exception as e: log.error('Test fail with: %s.' % e) @BeautifulReport.add_test_img('密码错误-登录失败') def test_b_login_fail(self): """ 登录-密码错误,登录失败 """ try: login = LoginPage() # 输入用户名,用户名不存在 login.input_username() # 输入密码 login.input_password(password='abcdefg') # 点击登录按钮 login.click_login_btn() time.sleep(2) # 保存图片 self.save_img(img_name='密码错误-登录失败') # 关闭提示 tip = TipPage() tip.click_btn(name='确定') time.sleep(5) log.info('Test pass') except Exception as e: log.error('Test fail with: %s.' % e) def test_login_success(self): """ 登录-用户名,密码正确,登录成功 """ login = LoginPage() login.input_username() login.input_password() login.click_login_btn() time.sleep(10) try: assert '登录成功' in login.check_login_success() log.info('Test pass') except Exception as e: log.error('Test fail with: %s.' % e) if __name__ == '__main__': pass
-
run_testcase.py
#!/usr/bin/env python # -*- coding: UTF-8 -*- import time import unittest import os from BeautifulReport import BeautifulReport as bf from common.browser_engine import BrowserEngine def all_case(): current_time = time.strftime("%Y-%m-%d-%H%M%S", time.localtime(time.time())) file_name = current_time + '-TestReport.html' case_path = os.path.join(os.getcwd(), "testcases") discover = unittest.defaultTestLoader.discover(case_path, pattern="test*.py") bf(discover).report(filename=file_name, description='测试报告', report_dir='reports') def driver_init(): BrowserEngine().open_browser() def driver_end(): BrowserEngine().close_browser() if __name__ == '__main__': #driver初始化 driver_init() #执行所有用例 all_case() #关闭浏览器 driver_end()
三、结果展示
四、关键代码分析
1. 在测试报告html中展示截图图片
self.save_img代码调用:
def save_img(self, img_name):
root_path = os.path.dirname(os.path.abspath(sys.argv[0]))
screenshots_name = os.path.join(root_path, 'img', f'{img_name}.png')
if os.path.exists(screenshots_name):
os.remove(screenshots_name)
try:
Common.driver.get_screenshot_as_file(screenshots_name)
except NameError as e:
log.error("failed to get windows img with %s" % e)
@BeautifulReport.add_test_img 装饰器代码分析:
def add_test_img(*pargs):
"""
接受若干个图片元素, 并展示在测试报告中
:param pargs:
:return:
"""
def _wrap(func):
@wraps(func)
def __wrap(*args, **kwargs):
img_path = os.path.abspath('{}'.format(BeautifulReport.img_path))
os.makedirs(img_path, exist_ok=True)
testclasstype = str(type(args[0]))
#print(testclasstype)
testclassnm = testclasstype[testclasstype.rindex('.') + 1:-2]
#print(testclassnm)
img_nm = testclassnm + '_' + func.__name__
#print(img_nm)
try:
result = func(*args, **kwargs)
except Exception:
#print("出现异常")
if 'save_img' in dir(args[0]):
#print("save_img")
save_img = getattr(args[0], 'save_img')
save_img(os.path.join(img_path, img_nm + '.png'))
data = BeautifulReport.img2base(img_path, img_nm + '.png')
print(HTML_IMG_TEMPLATE.format(data, data))
sys.exit(0)
print('<br></br>')
#print(img_path)
#print(pargs[0])
if len(pargs) > 1:
for parg in pargs:
print(parg + ':')
data = BeautifulReport.img2base(img_path, parg + '.png')
print(HTML_IMG_TEMPLATE.format(data, data))
return result
#print(img_path + pargs[0] + '.png')
if not os.path.exists(img_path +'\\'+ pargs[0] + '.png'):
return result
data = BeautifulReport.img2base(img_path, pargs[0] + '.png')
print(HTML_IMG_TEMPLATE.format(data, data))
return result
return __wrap
return _wrap
先调用save_img函数进行截图,存放路径必须是项目目录下的img目录,因为BeautifulReport库写的是img,然后BeautifulReport调用add_test_img(‘xxx’)函数时,就会取img目录的名称为xxx.png的图片,然后写进报告html中。
2. 使用共同的driver
其它地方调用:
3. ActionChains使用
#将某个元素拖拽到某个元素然后松开 ----> drag_and_drop(source, target)
def select_move_drop(self, source_selector, target_selector):
try:
dragElement=self.find_element(source_selector)
targetElement=self.find_element(target_selector)
Action=ActionChains(self.driver)
Action.drag_and_drop(dragElement,targetElement).perform()
self.sleep(5)
except Exception as e:
log.error("failed to drag and drop %s" % e)
def move_to_element_xy(self,to_element,x,y):
e=self.find_element(to_element)
ActionChains(self.driver).move_to_element_with_offset(e,x,y).click().perform()
4. XPATH
-
contains:
//div[@class='x-window x-layer x-window-default x-border-box'][last()]/div[contains(@id,'body')]
//tr/td/label[contains(text(),'{}')]/../../td/input
-
text:
//a/span/span/span[text()='{}']/..
-
starts-with:
//div[@class='x-panel x-border-item x-box-item x-panel-default']//div[starts-with(@id,'{}')]/div[contains(@id,'body')]
注意:没有ends-with
-
and:
//input[contains(@id,'fileuploadfield') and @name='filePath']
-
可以多次使用//
5. 元素
-
get_attribute():
value=e.get_attribute(keyword)
-
text:
e.text
-
is_displayed(): 是否显示
-
is_enabled(): 是否可编辑或者点击
-
is_selected(): 是否可选
6. 等待
-
智能等待
WebDriverWait(self.driver, 20).until(EC.presence_of_element_located((By.XPATH, xpath)))
-
隐式等待
#如果 WebDriver没有在 DOM中找到元素,将继续等待,超出设定时间后则抛出找不到元素的异常,换句话说,当查找元素或元素并没有立即出现的时候,隐式等待将等待一段时间再查找 DOM,默认的时间是0 self.driver.implicitly_wait(30) #隐式等待20秒,webdriver驱动器对象下的方法;针对是一次会话所有操作,相当于是一个全局等待;只需要声明定义一次,通常在设计脚本中会应用于setUp方法;
7. yaml
-
字典型,:后面需要空格
username: test
-
数组型,-后面需要空格
phone_book_add: - dispName: 显示名 - lastName: 姓 - firstName: 名 - gender: 女
-