python selenium+pytest webUI自动化基础框架
一、框架目录结构
cases:存放自动化测试用例脚本,脚本按业务模块划分子目录;
common:存放框架基础功能方法,如配置解析config.py等;
config:存放配置文件;
driver:存放浏览器webdriver;
page_element: 存放web页面元素对象;
pages:存放自动化测试用例脚本的基础方法封装,用例脚本由这些方法组合完成;
results:存放脚本日志及报告;
utils:存放工具类
二、common目录
1.readconfig.py
用于读取解析config下面的配置文件
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import configparser from config.conf import cm WEB_INFO = 'web info' LOGLEVEL = 'loglevel' FIREFOX_BIN = 'firefox_binary' TESTCASE = 'testcase' CONTROLLER_INFO = 'controller_info' 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 web_url(self): return self._get(WEB_INFO, 'web_url') @property def web_user(self): return self._get(WEB_INFO, 'web_user') @property def web_passwd(self): return self._get(WEB_INFO, 'web_passwd') @property def loglevel(self): return self._get(LOGLEVEL, LOGLEVEL).upper() @property def firefox_binary(self): return self._get(FIREFOX_BIN, FIREFOX_BIN) @property def web_broswer(self): return self._get(WEB_INFO, 'web_broswer') @property def testcase(self): return self._get(TESTCASE, TESTCASE).split(',') @property def controller_host(self): return self._get(CONTROLLER_INFO, 'controller_host') @property def controller_pwd(self): return self._get(CONTROLLER_INFO, 'controller_pwd') CONF = ReadConfig() if __name__ == '__main__': print(CONF.web_url)
2.readelement.py
用于读取page_element目录下元素定位对象yaml文件配置
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import os import yaml 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 return name, value raise ArithmeticError("{}中不存在关键字:{}".format(self.file_name, item)) if __name__ == '__main__': search = Element('search') print(search['搜索框'])
3.readtestdata.py
用于读取testdata目录下数据驱动配置的Excel数据
1 #!/usr/bin/env python3 2 # -*- coding:utf-8 -*- 3 import os 4 5 import pandas as pd 6 from config.conf import cm 7 from utils.utils import gen_random_string 8 9 10 class TestData(): 11 """ 12 功能:获取excel文件数据 13 参数:name:excel文件名称,不包括后缀 14 caseid:与Excel文件sheet页名称相同,建议以用例ID命名 15 返回:以列表形式返回对应sheet页所有数据 16 """ 17 18 def __init__(self, name): 19 self.file_name = '%s.xls' % name 20 self.testdata_path = os.path.join(cm.TESTDATA_PATH, self.file_name) 21 if not os.path.exists(self.testdata_path): 22 raise FileNotFoundError("%s 文件不存在!" % self.testdata_path) 23 self.testdata = pd.ExcelFile(self.testdata_path, engine='xlrd') 24 25 def __call__(self, caseid): 26 return self.testdata.parse(str(caseid), keep_default_na=False).values.tolist()
三、config目录
1.config.ini
配置一些环境及用例信息
2.conf.py
存储项目相关目录结构
1 #!/usr/bin/env python3 2 # -*- coding:utf-8 -*- 3 import os 4 from selenium.webdriver.common.by import By 5 from utils.times import dt_strftime 6 7 8 class ConfigManager(object): 9 # 项目目录 10 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 11 12 # 页面元素目录 13 ELEMENT_PATH = os.path.join(BASE_DIR, 'page_element') 14 15 # 测试数据目录 16 TESTDATA_PATH = os.path.join(BASE_DIR, 'testdata') 17 18 # 报告文件 19 REPORT_PATH = os.path.join(BASE_DIR, 'results', 'reports') 20 21 # driver目录 22 DRIVER_PATH = os.path.join(BASE_DIR, 'driver') 23 24 # case目录 25 CASE_PATH = os.path.join(BASE_DIR, 'cases') 26 27 # 元素定位的类型 28 LOCATE_MODE = { 29 'css': By.CSS_SELECTOR, 30 'xpath': By.XPATH, 31 'name': By.NAME, 32 'id': By.ID, 33 'class': By.CLASS_NAME 34 } 35 36 @property 37 def allure_json(self): 38 return os.path.join(cm.REPORT_PATH, 'allure_json') 39 40 @property 41 def allure_xml(self): 42 return os.path.join(cm.REPORT_PATH, 'allure_xml') 43 44 @property 45 def pytest_html(self): 46 return os.path.join(cm.REPORT_PATH, 'pytest_html') 47 48 @property 49 def log_file(self): 50 """日志目录""" 51 log_dir = os.path.join(self.BASE_DIR, 'results', 'logs') 52 if not os.path.exists(log_dir): 53 os.makedirs(log_dir) 54 return os.path.join(log_dir, '{}.log'.format(dt_strftime('%Y%m%d'))) 55 56 @property 57 def ini_file(self): 58 """配置文件""" 59 ini_file = os.path.join(self.BASE_DIR, 'config', 'config.ini') 60 if not os.path.exists(ini_file): 61 raise FileNotFoundError("配置文件%s不存在!" % ini_file) 62 return ini_file 63 64 65 cm = ConfigManager() 66 if __name__ == '__main__': 67 print(cm.BASE_DIR)
四、driver目录
1.chromedriver
谷歌浏览器驱动文件
2.gechodriver
火狐浏览器驱动文件
五、page_element目录
页面元素定位配置
按照页面模块创建不同的yaml文件来保存页面元素定位对象
如base.yaml,保存的是可共用(多个模块可用的)页面元素定位信息
六、pages目录
页面对象方法
1.base_page.py
selenium相关的基础方法,如元素定位、登录等,以及其他页面模块可共用的方法
1 #!/usr/bin/env python3 2 # -*- coding=utf-8 -*- 3 4 ######################################################### 5 # file: base_page.py 6 # func: web UI 自动化基础接口封装 7 # author: weiduntao 8 # date: 2021.12.24 9 ######################################################### 10 import os 11 import allure 12 from urllib import parse 13 from selenium import webdriver 14 from selenium.webdriver.support.ui import WebDriverWait 15 from selenium.webdriver.support.select import Select 16 from selenium.webdriver.support import expected_conditions as EC 17 from selenium.common.exceptions import TimeoutException 18 from selenium.webdriver.firefox.options import Options as firefox_op 19 from selenium.webdriver.firefox.firefox_binary import FirefoxBinary 20 from selenium.webdriver.firefox.service import Service 21 from selenium.webdriver import ActionChains 22 from selenium.webdriver.common.keys import Keys 23 from selenium.webdriver.common.by import By 24 from selenium.webdriver.remote.webelement import WebElement 25 from selenium.webdriver.chrome.options import Options as chrome_op 26 from config.conf import cm 27 from utils.times import sleep, dt_strftime 28 from utils.logger import Log 29 from common.readelement import Element 30 from common.readconfig import CONF 31 from preset_up import PreSet 32 import inspect 33 34 base = Element('base') 35 preset = PreSet() 36 37 38 class BasePage(): 39 """selenium基类""" 40 41 def __init__(self, driver=None): 42 self.log = Log().logger 43 self.report = cm.allure_json 44 self.broswer = CONF.web_broswer or 'firefox' 45 self.base_url = CONF.web_url 46 self.timeout = 6 47 if driver is None: 48 self.set_driver(self.broswer) 49 else: 50 self.driver = driver 51 self.wait = WebDriverWait(self.driver, self.timeout) 52 self.action_chain = ActionChains(self.driver) 53 self.refresh() 54 55 def set_driver(self, driver): 56 if 'chrome' == driver.lower().strip(): 57 options = chrome_op() 58 options.add_argument('--start-maximized') 59 options.add_argument("--ignore-certificate-errors") 60 options.add_argument("--ignore-ssl-errors") 61 self.driver = webdriver.Chrome(os.path.join(cm.DRIVER_PATH, 'chromedriver'), 62 chrome_options=options) 63 elif 'firefox' == driver.lower().strip(): 64 binary_file = CONF.firefox_binary or '/usr/bin/firefox-esr' 65 executable_path = os.path.join(cm.DRIVER_PATH, 'geckodriver') 66 options = firefox_op() 67 options.binary = FirefoxBinary(binary_file) 68 service = Service(executable_path=executable_path) 69 my_profile = webdriver.FirefoxProfile() 70 my_profile.accept_untrusted_certs = True 71 self.driver = webdriver.Firefox(firefox_profile=my_profile, options=options, 72 service=service) 73 self.driver.maximize_window() 74 else: 75 raise Exception('暂不支持%s浏览器驱动' % driver) 76 self.wait = WebDriverWait(self.driver, self.timeout) 77 self.action_chain = ActionChains(self.driver) 78 79 def check_driver_quit(self): 80 """校验webdriver是否quit""" 81 if self.driver.service.process is not None: 82 return False 83 return True 84 85 def get_url(self, url): 86 """打开网址并验证""" 87 url = parse.urljoin(self.base_url, url) 88 self.driver.set_page_load_timeout(60) 89 try: 90 self.driver.implicitly_wait(self.timeout) 91 self.driver.get(url) 92 self.log.info("打开网页:%s" % url) 93 except TimeoutException: 94 self.driver.quit() 95 raise TimeoutException("打开%s超时请检查网络或网址服务器" % url) 96 97 def login(self, username=None, passwd=None): 98 # self.set_driver(self.broswer) 99 self.get_url('/auth/login') 100 username = username or CONF.web_user 101 passwd = passwd or CONF.web_passwd 102 self.input_text(base['用户名'], username) 103 self.input_text(base['密码'], passwd) 104 sleep() 105 self.click(base['登录']) 106 self.log.info("用户:%s登录网页:%s成功" % (username, self.base_url)) 107 108 def relogin(self, user=preset.pre_set_data['user'], passwd=preset.pre_set_data['passwd']): 109110 # date: 2022.03.03 111 self.personal_center('登出') 112 sleep() 113 self.input_text(base['用户名'], user) 114 self.input_text(base['密码'], passwd) 115 sleep() 116 self.click(base['登录']) 117 self.log.info("用户:%s登录网页:%s成功" % (user, self.base_url)) 118 119 def personal_center(self, operation): 120 """点击页面右上角-用户名-进入个人中心""" 121 # 参数operation表示个人中心的操作,可选个人信息、修改密码、登出 122 self.log.info(inspect.getdoc(self.personal_center) + '-' + operation) 123 try: 124 self.click(base['个人中心']) 125 self.click(base['个人中心_%s' % operation]) 126 except: 127 self.assert_result(False, msg='[ERROR]点击个人中心-%s操作失败' % operation) 128 129 @staticmethod 130 def element_locator(func, locator): 131 """元素定位器""" 132 name, value = locator 133 return func(cm.LOCATE_MODE[name], value) 134 135 def find_element(self, locator): 136 """寻找单个元素""" 137 return BasePage.element_locator(lambda *args: self.wait.until( 138 EC.presence_of_element_located(args)), locator) 139 140 def find_elements(self, locator): 141 """查找多个相同的元素""" 142 return BasePage.element_locator(lambda *args: self.wait.until( 143 EC.presence_of_all_elements_located(args)), locator) 144 145 def elements_num(self, locator): 146 """获取相同元素的个数""" 147 number = len(self.find_elements(locator)) 148 self.log.info("相同元素:{}".format((locator, number))) 149 return number 150 151 def find_element_clickable(self, locator): 152 """寻找单个可点击的元素""" 153 return BasePage.element_locator(lambda *args: self.wait.until( 154 EC.element_to_be_clickable(args)), locator) 155 156 def find_element_by_element(self, element, locator): 157 """通过已知元素查找相关元素,locator""" 158 name, value = locator 159 if name != By.XPATH: 160 raise Exception('此方法只支持xpath定位方式') 161 if value.startswith('/'): 162 value = '.' + value 163 return WebDriverWait(element, self.timeout).until(EC.presence_of_element_located((name, 164 value))) 165 166 def alert_is_present(self): 167 """告警窗口出现""" 168 return EC.alert_is_present()(self.driver) 169 170 def input_text(self, locator, txt): 171 """输入(输入前先清空)""" 172 if isinstance(locator, WebElement): 173 ele = locator 174 else: 175 ele = self.find_element(locator) 176 sleep(0.5) 177 ele.clear() 178 sleep(0.5) 179 if txt == '': 180 ele.send_keys('a') 181 sleep(0.5) 182 ele.send_keys(Keys.BACKSPACE) 183 sleep(0.5) 184 ele.send_keys(txt) 185 self.log.info("输入文本:{}".format(txt)) 186 return ele 187 188 def reset_actions(self): 189 """清除存储在远端的动作""" 190 self.action_chain.reset_actions() 191 sleep(1) 192 193 def click(self, locator): 194 """点击""" 195 if isinstance(locator, WebElement): 196 _ele = locator 197 else: 198 _ele = self.find_element_clickable(locator) 199 _ele.click() 200 sleep() 201 self.log.info("点击元素:{}".format(locator)) 202 return _ele 203 204 def wait_alert(self, interval=0.5, wait_count=10): 205 """等待告警窗口出现""" 206 while wait_count > 0: 207 result = self.alert_is_present() 208 if result is not False: 209 return result 210 sleep(interval) 211 wait_count -= 1 212 raise Exception("等待告警窗口超时") 213 214 def get_alert_text(self, interval=0.5, wait_count=10): 215 """获取alert告警内容""" 216 alert = self.wait_alert(interval=interval, wait_count=wait_count) 217 self.log.info("获取alert告警内容成功") 218 return alert.text 219 220 def accept_alert(self, interval=0.5, wait_count=10): 221 """告警确定""" 222 alert = self.wait_alert(interval=interval, wait_count=wait_count) 223 content = alert.text 224 alert.accept() 225 self.log.info("告警窗口确定:{}".format(alert)) 226 return content 227 228 def dismis_alert(self, interval=0.5, wait_count=10): 229 """告警取消""" 230 alert = self.wait_alert(interval=interval, wait_count=wait_count) 231 content = alert.text 232 alert.dismiss() 233 self.log.info("告警窗口取消:{}".format(alert)) 234 return content 235 236 def hover(self, locator): 237 """鼠标悬停""" 238 if isinstance(locator, WebElement): 239 _ele = locator 240 else: 241 _ele = self.find_element(locator) 242 self.action_chain.move_to_element(_ele) 243 # self.action_chain.click(_ele) 244 self.action_chain.perform() 245 sleep() 246 self.log.info("悬停元素:{}".format(locator)) 247 return _ele 248 249 def move_mouse(self, xoffset, yoffset): 250 """鼠标移动到距离当前位置(x,y)""" 251 self.action_chain.move_by_offset(xoffset, yoffset).perform() 252 sleep() 253 self.log.info('鼠标移动到距离当前位置({},{})'.format(xoffset, yoffset)) 254 255 def enter(self, locator): 256 if isinstance(locator, WebElement): 257 _ele = locator 258 else: 259 _ele = self.find_element(locator) 260 _ele.send_keys(Keys.ENTER) 261 sleep() 262 self.log.info("回车元素:{}".format(locator)) 263 return _ele 264 265 def element_text(self, locator): 266 """获取当前的text""" 267 if isinstance(locator, WebElement): 268 _ele = locator 269 else: 270 _ele = self.find_element(locator) 271 _text = _ele.text 272 self.log.info("获取文本:{}".format(_text)) 273 return _text.strip() 274 275 def assert_result(self, result, expect=True, not_negative=True, msg=''): 276 """断言,not_negative=True表示正向逻辑""" 277 if isinstance(expect, bool): 278 if not_negative and (result == expect): 279 assert True 280 elif (not not_negative) and (result != expect): 281 assert True 282 else: 283 self.save_screenshot(msg) 284 self.log.error(msg, exc_info=True) 285 assert False 286 else: 287 if not_negative and (expect in result): 288 assert True 289 elif (not not_negative) and (expect not in result): 290 assert True 291 else: 292 self.save_screenshot(msg) 293 self.log.error(msg) 294 assert False 295 296 @property 297 def get_source(self): 298 """获取页面源代码""" 299 return self.driver.page_source 300 301 def refresh(self): 302 """刷新页面F5""" 303 self.driver.refresh() 304 sleep(2) 305 306 def save_screenshot(self, msg): 307 """截图并保存""" 308 if '/' in msg: 309 msg = msg.replace('/', '-') 310 file_name = dt_strftime(fmt='%Y%m%d%H%M%S') + '-' + str(msg) + '.png' 311 file = os.path.join(self.report, file_name) 312 self.driver.save_screenshot(file) 313 allure.attach.file(file, file_name, attachment_type=allure.attachment_type.PNG) 314 315 @property 316 def success(self): 317 return True 318 319 @property 320 def fail(self): 321 return False 322 323 def select_cloud_type(self, cloud_type='云平台'): 324 """云平台首页选择类型:用户中心、云平台或者运维中心""" 325 if cloud_type not in ('用户中心', '云平台', '运维中心'): 326 raise Exception('云平台类型只能是“用户中心、云平台或者运维中心”') 327 self.click(base['平台类型选择']) 328 self.click(base[cloud_type]) 329 self.log.info('选择:{}'.format(cloud_type)) 330 331 def select(self, locator, by_type, type_val): 332 """ 333 下拉菜单选择 334 by_type: value --> select_by_value 335 index --> select_by_index 336 text --> select_by_visible_text 337 type_val: 对应于by_type的对象值 338 """ 339 if isinstance(locator, WebElement): 340 _ele = locator 341 else: 342 _ele = self.find_element(locator) 343 s = Select(_ele) 344 if by_type == 'value': 345 s.select_by_value(type_val) 346 elif by_type == 'index': 347 s.select_by_index(type_val) 348 elif by_type == 'text': 349 s.select_by_visible_text(type_val) 350 else: 351 raise Exception('by_type is enum:value/index/text') 352 self.log.info('下拉菜单选择:{}'.format(type_val)) 353 return s.all_selected_options 354 355 def deselect(self, locator, by_type, type_val): 356 """ 357 下拉菜单去勾选 358 by_type: value --> select_by_value 359 index --> select_by_index 360 text --> select_by_visible_text 361 type_val: 对应于by_type的对象值 362 """ 363 if isinstance(locator, WebElement): 364 _ele = locator 365 else: 366 _ele = self.find_element(locator) 367 s = Select(_ele) 368 if by_type == 'value': 369 s.deselect_by_value(type_val) 370 elif by_type == 'index': 371 s.deselect_by_index(type_val) 372 elif by_type == 'text': 373 s.deselect_by_visible_text(type_val) 374 else: 375 raise Exception('by_type is enum:value/index/text') 376 self.log.info('下拉菜单去勾选:{}'.format(type_val)) 377 return s.all_selected_options 378 379 def table(self, locator): 380 """table表格""" 381 if isinstance(locator, WebElement): 382 _ele = locator 383 else: 384 _ele = self.find_element(locator) 385 tr_list = _ele.find_elements(By.TAG_NAME, 'tr') 386 table_info = [] 387 table_info.append(tr_list[0].find_elements(By.TAG_NAME, 'th')) 388 for tr in tr_list[1:]: 389 table_info.append(tr.find_elements(By.TAG_NAME, 'td')) 390 self.log.info('获取table表格:{}'.format(locator)) 391 return table_info 392 393 def elements_li(self, locator, li_text) -> WebElement: 394 """ul/ol 中的li列表元素""" 395 if isinstance(locator, WebElement): 396 _ele = locator 397 else: 398 _ele = self.find_element(locator) 399 self.log.info('获取列表元素:{}'.format(locator)) 400 lis = _ele.find_elements(By.TAG_NAME, 'li') 401 for li in lis: 402 if li.text.strip() == li_text.strip(): 403 return li 404 raise Exception('列表中未找到%s元素' % li_text) 405 406 def select_radio_or_checkbox(self, locator): 407 """勾选单选框或复选框""" 408 if isinstance(locator, WebElement): 409 _ele = locator 410 else: 411 _ele = self.find_element(locator) 412 if _ele.is_selected(): 413 self.log.info('勾选选择框:{}'.format(locator)) 414 return 415 self.click(_ele) 416 if not _ele.is_selected(): 417 self.log.info('勾选选择框:{}失败'.format(locator)) 418 raise Exception('勾选选择框:{}失败'.format(locator)) 419 self.log.info('勾选选择框:{}'.format(locator)) 420 421 def deselect_checkbox(self, locator): 422 """去勾选复选框""" 423 if isinstance(locator, WebElement): 424 _ele = locator 425 else: 426 _ele = self.find_element(locator) 427 if _ele.is_selected(): 428 self.click(_ele) 429 if _ele.is_selected(): 430 self.log.info('去勾选选择框:{}失败'.format(locator)) 431 raise Exception('去勾选选择框:{}失败'.format(locator)) 432 self.log.info('去勾选选择框:{}'.format(locator)) 433 434 def select_by_noselector(self, locator, tag_name, attr_name, *attr_value): 435 """ 436 无select标签的下拉菜单选项 437 可以通过标签的属性值或者text来定位,也可以通过index来定位 438 """ 439 if isinstance(locator, WebElement): 440 _ele = locator 441 else: 442 _ele = self.find_element(locator) 443 sels = _ele.find_elements(By.TAG_NAME, tag_name) 444 selected = [] 445 for val in attr_value: 446 if isinstance(val, int): 447 self.click(sels[val]) 448 selected.append(val) 449 self.log.info('下拉菜单选择第{}个'.format(val)) 450 else: 451 for sel in sels: 452 sel_v = sel.get_attribute(attr_name) 453 if sel_v is None: 454 sel_v = sel.text 455 if sel_v.strip() == str(val).strip(): 456 self.click(sel) 457 selected.append(sel_v) 458 self.log.info('下拉菜单选择:{}'.format(val)) 459 break 460 if len(selected) != len(attr_value): 461 raise Exception( 462 '如下菜单选项未被点击选中:{}'.format([i for i in attr_value if i not in selected])) 463 464 def check_confirm_window(self, confirm_content): 465 """判断是否弹出确认窗口,如删除XX确认窗口""" 466 confirm_window = self.find_element(base['确认弹窗']) 467 if confirm_content.strip() not in confirm_window.text: 468 raise Exception('弹窗不正确,期望弹窗内容:{},实际弹窗内容:{}'.format(confirm_content, 469 confirm_window.text.strip())) 470 return confirm_window 471 472 @allure.step('点击确定') 473 def accept_confirm(self): 474 """点击确定""" 475 try: 476 self.click(base['确认']) 477 self.log.info('点击确定') 478 sleep(2) 479 except: 480 self.assert_result(False, msg='点击确定失败') 481 482 @allure.step('点击取消') 483 def dismis_confirm(self): 484 """点击取消""" 485 try: 486 self.click(base['取消']) 487 self.log.info('点击取消') 488 except: 489 self.assert_result(False, msg='点击取消失败') 490 491 def get_dd_by_dt(self, locator, *dt): 492 """HTML中根据dl中dt值获取对应dd值""" 493 _, val = locator 494 if not val.endswith('dl'): 495 raise Exception('locator必须定位到"dl"处') 496 dts = {} 497 _eles = self.find_elements(locator) 498 for i in dt: 499 for ele in _eles: 500 ele_text = ele.text 501 ele_dt = ele_text.split(':')[0].strip() 502 if ele_dt == i.strip(): 503 ele_dd = ele_text.split(':')[1].strip() 504 self.log.info('{}:{}'.format(ele_dt, ele_dd)) 505 dts.update({ele_dt: ele_dd}) 506 break 507 if len(dts) == len(dt): 508 return dts 509 not_in = [] 510 for i in dt: 511 if i.strip() not in dts: 512 not_in.append(i) 513 raise Exception('dt:{}不存在,请检查参数'.format(not_in)) 514 515 def match_by_name(self, table_info, match_word, primary_td=2): 516 """默认返回名称为match_word或第match_word行""" 517 if isinstance(match_word, int): 518 return table_info[match_word] 519 for tr in table_info: 520 if tr[primary_td].text == match_word: 521 self.log.info('找到 %s 元素位置' % match_word) 522 return tr 523 raise Exception('未找到名称为:%s 元素行' % match_word) 524 525 @allure.step('选择搜索类型') 526 def select_search_type(self, search_type='名称'): 527 """选择搜索类型""" 528 try: 529 self.hover(base['检索类型']) 530 search_type_list = self.element_text(base['检索类型列表']) 531 if search_type in search_type_list: 532 ele_loc = self.elements_li(base['检索类型列表'], search_type) 533 self.click(ele_loc) 534 self.move_mouse(0, -50) 535 else: 536 raise Exception('检索类型异常') 537 except: 538 self.assert_result(False, msg='[ERROR]选择搜索类型失败') 539 540 @allure.step('输入查询关键字') 541 def input_search_word(self, input_word): 542 """输入查询关键字""" 543 try: 544 _ele = self.input_text(base['输入查询关键字'], input_word) 545 self.enter(_ele) 546 sleep() 547 except: 548 self.assert_result(False, msg='[ERROR]输入查询关键字失败') 549 550 @allure.step('清空检索项') 551552 # date: 2022.03.29 553 def clear_search(self): 554 """清空检索项""" 555 try: 556 self.click(base['清空检索项']) 557 sleep() 558 except: 559 self.assert_result(False, msg='[ERROR]清空检索项失败') 560 561 @allure.step('选择某项属性,正序/倒叙/默认状态') 562 def adjust_sortable(self, sort_name, sort_type=None): 563 """选择某项属性,正序/倒叙/默认状态,适用于网络、子网、端口界面""" 564 try: 565 self.sort_type = sort_type or '默认' 566 th_list = self.find_elements(base['表头']) 567 if self.sort_type == '升序': 568 self.sort_type = 'cell sortable ascending' 569 elif self.sort_type == '降序': 570 self.sort_type = 'cell sortable descending' 571 else: 572 self.sort_type = 'cell sortable' 573 for th in th_list: 574 loc = self.find_element_by_element(th, base['类名']) 575 if loc.text == sort_name: 576 while self.sort_type != th.get_attribute('class').strip(): 577 self.click(th) 578 sleep(1) 579 sort_status = th.get_attribute('class') 580 if self.sort_type != sort_status: 581 raise Exception("预期排序“{}”,与实际排序“{}”不符".format(self.sort_type, sort_status)) 582 except: 583 self.assert_result(False, msg="选择某项属性,正序/倒叙/默认状态") 584 585 @allure.step('获取当前界面非法输入提示信息') 586 def get_validation_message(self): 587 """获取当前界面非法输入提示信息""" 588 try: 589 messages = self.find_elements(base['验证消息']) 590 meg_list = [] 591 for meg in messages: 592 meg_list.append(self.element_text(meg).strip()) 593 return ''.join(meg_list) 594 except: 595 self.assert_result(False, msg="[ERROR]当前界面无验证消息") 596 597 @allure.step('查找当前界面中置灰的按钮') 598 def find_disable_eles(self): 599 """查找当前界面中置灰的按钮""" 600 try: 601 ele_list = self.find_elements(base['置灰按钮']) 602 ele_texts = [] 603 for ele in ele_list: 604 ele_texts.append(self.element_text(ele).strip()) 605 self.log.info("置灰按钮:{}".format(ele_texts)) 606 return ''.join(ele_texts) 607 except: 608 self.assert_result(False, msg="[ERROR]查找当前界面中置灰的按钮失败") 609 610 @allure.step('校验当前界面中置灰的按钮') 611 def check_disable_eles(self, ele='确定'): 612 """校验当前界面中置灰的按钮""" 613 try: 614 eles = self.find_disable_eles() 615 if ele not in eles: 616 raise Exception("置灰按钮校验失败:预期:{},实际:{}".format(ele, eles)) 617 return True 618 except: 619 self.assert_result(False, msg="置灰按钮校验失败,期望:{}".format(ele)) 620 621 @allure.step('校验非法输入提示信息') 622 def check_input_illegal(self, message): 623 """校验非法输入提示信息""" 624 try: 625 msgs = self.get_validation_message() 626 if message not in msgs: 627 raise Exception("非法输入提示信息校验失败:预期:{},实际:{}".format(message, msgs)) 628 except: 629 self.assert_result(False, msg="非法输入提示信息校验失败,期望:{}".format(message)) 630 631 @allure.step('获取select列表的值') 632 def get_select_list(self, locator): 633 """获取select列表的值""" 634 if isinstance(locator, WebElement): 635 _ele = locator 636 else: 637 _ele = self.find_element(locator) 638 s = Select(_ele) 639 s_list = [] 640 for opt in s.options: 641 s_list.append(opt.text) 642 return s_list 643 644 @allure.step('查找当前界面中隐藏的按钮') 645 def find_hidden_eles(self): 646 """查找当前界面中隐藏的按钮""" 647 try: 648 ele_list = self.find_elements(base['隐藏按钮']) 649 ele_texts = [] 650 for ele in ele_list: 651 eles_list = ele.get_attribute("innerText").strip().split("\n") 652 for eles in eles_list: 653 ele_texts.append(eles) 654 self.log.info("隐藏按钮:{}".format(ele_texts)) 655 return ele_texts 656 except: 657 self.assert_result(False, msg="[ERROR]查找当前界面中隐藏的按钮失败") 658 659 @allure.step('校验当前界面中隐藏的按钮') 660 def check_hidden_eles(self, ele=None): 661 """校验当前界面中隐藏的按钮""" 662 try: 663 eles = self.find_hidden_eles() 664 if ele not in eles: 665 return False 666 return True 667 except: 668 self.assert_result(False, msg="隐藏按钮校验失败,期望:{}".format(ele)) 669 670 @allure.step('获取当前用户') 671 def get_present_user(self): 672 """获取当前用户""" 673 try: 674 user = self.element_text(base['当前用户']) 675 return user 676 except: 677 self.assert_result(False, msg="获取当前用户失败")
2.其他xx_page.py继承自base_page.py,然后封装自己的业务方法
七、results目录
测试报告及日志路径
1.logs
保存logging模块打印的日志
2.reports.allure_json
保存allure json格式的报告
3.reports.allure_xml
保存allure xml格式的报告
4.reports.pytest_html
保存pytest HTML报告
八、testdata目录
测试数据存放位置
其中xls文件用于保存测试用例数据驱动配置
九、utils目录
1.logger.py
全局logging
1 #!/usr/bin/env python3 2 # -*- coding:utf-8 -*- 3 import logging 4 from config.conf import cm 5 from common.readconfig import CONF 6 7 8 class Log(): 9 def __init__(self): 10 self.logger = logging.getLogger() 11 self.logger.setLevel(CONF.loglevel) 12 if self.logger.handlers: 13 self.logger.handlers.clear() 14 15 # 创建一个handle写入文件 16 fh = logging.FileHandler(cm.log_file, encoding='utf-8') 17 fh.setLevel(CONF.loglevel) 18 19 # 创建一个handle输出到控制台 20 ch = logging.StreamHandler() 21 ch.setLevel(CONF.loglevel) 22 23 # 定义输出的格式 24 formatter = logging.Formatter(self.fmt) 25 fh.setFormatter(formatter) 26 ch.setFormatter(formatter) 27 28 # 添加到handle 29 self.logger.addHandler(fh) 30 self.logger.addHandler(ch) 31 32 @property 33 def fmt(self): 34 return '%(levelname)s\t%(asctime)s\t[%(filename)s:%(lineno)d]\t%(message)s' 35 36 37 if __name__ == '__main__': 38 log = Log().logger 39 log.info('hello world')
2.times.py
时间相关方法
1 #!/usr/bin/env python3 2 # -*- coding:utf-8 -*- 3 import time 4 import datetime 5 from functools import wraps 6 7 8 def timestamp(): 9 """时间戳""" 10 return time.time() 11 12 13 def dt_strftime(fmt="%Y%m"): 14 """ 15 datetime格式化时间 16 :param fmt "%Y%m%d%H%M%S 17 """ 18 return datetime.datetime.now().strftime(fmt) 19 20 21 def sleep(seconds=1.0): 22 """ 23 睡眠时间 24 """ 25 time.sleep(seconds) 26 27 28 def running_time(func): 29 """函数运行时间""" 30 31 @wraps(func) 32 def wrapper(*args, **kwargs): 33 start = timestamp() 34 res = func(*args, **kwargs) 35 print("校验元素done!用时%.3f秒!" % (timestamp() - start)) 36 return res 37 38 return wrapper 39 40 41 if __name__ == '__main__': 42 print(dt_strftime("%Y%m%d%H%M%S"))
3.utils.py
通用工具方法
1 #!/usr/bin/env python3 2 # -*- coding=utf-8 -*- 3 import random 4 import string 5 import time 6 7 8 def gen_random_string(pre_fix='test_', str_len=10): 9 return pre_fix + ''.join( 10 random.choice(string.digits + string.ascii_letters) for _ in range(str_len - len(pre_fix))) 11 12 13 def gen_random_num(pre_fix='181', str_len=11): 14 return pre_fix + ''.join(random.choice(string.digits) for _ in range(str_len - len(pre_fix))) 15 16 17 def replace_file_content(file, old_content, new_content): 18 """ 19 替换文件内容 20 file: 要替换内容的文件路径 21 old_content:要替换的内容,可迭代对象,如列表等 22 new_content:替换后的内容,可迭代对象,如列表等 23 """ 24 replaces = zip(old_content, new_content) 25 with open(file, encoding='utf-8') as fr: 26 content = fr.read() 27 for old, new in replaces: 28 content = content.replace(old, new) 29 with open(file, 'w', encoding='utf-8') as fw: 30 fw.write(content) 31 32 33 def unicode(): 34 val = random.randint(0x4e00, 0x9fbf) 35 return chr(val) 36 37 38 def create_name(n): 39 """随机生成n个汉字""" 40 name = '' 41 for i in range(n): 42 s = unicode() 43 name = name + s 44 return name 45 46 47 def create_digits(n): 48 """随机生成n个特殊字符""" 49 seed = "!@#$%^&*()+=-" 50 sa = [] 51 for i in range(n): 52 sa.append(random.choice(seed)) 53 salt = ''.join(sa) 54 return salt 55 56 57 def create_letters(n): 58 """随机生成n个随机生成英文字母""" 59 name = '' 60 for i in range(n): 61 s = random.choice(string.ascii_letters) 62 name = name + s 63 return name 64 65 66 def wait_util_success(count=10, interval=0.5): 67 """装饰器,用作循环等待函数执行成功,若超时则抛异常""" 68 69 def wrapper(func): 70 def decorater(*args, **kwargs): 71 for i in range(count): 72 try: 73 return func(*args, **kwargs) 74 except Exception as e: 75 if i == count - 1: 76 raise Exception(e) 77 time.sleep(interval) 78 79 return decorater 80 81 return wrapper 82 83 84 if __name__ == '__main__': 85 replace_file_content( 86 'results/reports/allure_json/207b0bb4-9969-4e95-bbfa-90b2210a429d-attachment.txt', 87 ['[32m', '[0m', '[1m', '[31m', '[33m'], ['' for _ in range(5)])