Python+Selenium+Pytest+POM自动化测试框架封装(完备版)

[复制链接]
发表于 2025-9-20 21:10:25 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快    
1、测试框架简介

1)测试框架的长处

  • 代码复用率高,假如不利用框架的话,代码会显得很冗余。
  • 可以组装日志日志、陈诉、邮件等一些高级功能。
  • 提高元素等数据的可维护性,元素发生变化时,只须要更新一下设置文件。
  • 利用更灵活的PageObject筹划模式。
2)测试框架的整体目次

【留意】 init.py 文件用以标识此目次为一个python包。
2、起首时间管理

起首,因为许多的模块都会用到当前时间的时间戳,大概日期等等字符串,以是先单独把时间操纵(我们须要获取的差别格式的时间信息)封装成一个模块。
然后让其他模块来调用即可。在 utils 目次新建 times.py 模块。
  1. #!/usr/bin/env python3
  2. # -*- coding:utf-8 -*-
  3. import time
  4. import datetime
  5. from functools import wraps
  6. def timestamp():
  7.     """时间戳"""
  8.     return time.time()
  9. def dt_strftime(fmt="%Y%m"):
  10.     """
  11.     datetime格式化时间
  12.     :param fmt "%Y%m%d %H%M%S
  13.     """
  14.     return datetime.datetime.now().strftime(fmt)
  15. def sleep(seconds=1.0):
  16.     """
  17.     睡眠时间
  18.     """
  19.     time.sleep(seconds)
  20. def running_time(func):
  21.     """函数运行时间"""
  22.     @wraps(func)
  23.     def wrapper(*args, **kwargs):
  24.         start = timestamp()
  25.         res = func(*args, **kwargs)
  26.         print("校验元素done!用时%.3f秒!" % (timestamp() - start))
  27.         return res
  28.     return wrapper
  29. if __name__ == '__main__':
  30.     print(dt_strftime("%Y%m%d%H%M%S"))
复制代码
3、添加设置文件

1)conf.py
UI自动化测试框架中应该有一个文件对整体的目次举行管理;
在项目中的 config 目次创建 conf.py 文件,全部的目次设置信息写在这个文件内里。
  1. import os
  2. from selenium.webdriver.common.by import By
  3. from utils.times import dt_strftime
  4. class ConfigManager(object):
  5.     # 项目目录
  6.     BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  7.     # 页面元素目录
  8.     ELEMENT_PATH = os.path.join(BASE_DIR, 'page_element')
  9.     # 报告文件
  10.     REPORT_FILE = os.path.join(BASE_DIR, 'report.html')
  11.     # 元素定位的类型
  12.     LOCATE_MODE = {
  13.         'css': By.CSS_SELECTOR,
  14.         'xpath': By.XPATH,
  15.         'name': By.NAME,
  16.         'id': By.ID,
  17.         'class': By.CLASS_NAME
  18.     }
  19.     # 邮件信息
  20.     EMAIL_INFO = {
  21.         'username': '[email protected]',  # 切换成你自己的地址
  22.         'password': 'QQ邮箱授权码',
  23.         'smtp_host': 'smtp.qq.com',
  24.         'smtp_port': 465
  25.     }
  26.     # 收件人
  27.     ADDRESSEE = [
  28.         '[email protected]',
  29.     ]
  30.     @property
  31.     def log_file(self):
  32.         """日志日志目录"""
  33.         log_dir = os.path.join(self.BASE_DIR, 'logs')
  34.         if not os.path.exists(log_dir):
  35.             os.makedirs(log_dir)
  36.         return os.path.join(log_dir, '{}.log'.format(dt_strftime()))
  37.     @property
  38.     def ini_file(self):
  39.         """配置文件"""
  40.         ini_file = os.path.join(self.BASE_DIR, 'config', 'config.ini')
  41.         if not os.path.exists(ini_file):
  42.             raise FileNotFoundError("配置文件%s不存在!" % ini_file)
  43.         return ini_file
  44. cm = ConfigManager()
  45. if __name__ == '__main__':
  46.     print(cm.BASE_DIR)
复制代码
【留意】:QQ邮箱授权码:点击检察天生教程
2)config.ini
在项目 config 目次新建一个 config.ini 文件,内里临时先放入须要测试的URL。
  1. [HOST]
  2. HOST = https://www.baidu.com
复制代码
3)读取设置文件
①设置文件创建好了,接下来我们须要读取这个设置文件以利用内里的信息。
②利用python内置的 configparser 模块对 config.ini 文件信息举行了读取。
③对于测试url值的提取,利用python高阶语法 @property 属性值,写法更简单。
④在 common 目次中新建一个 readconfig.py 文件。
  1. import configparser
  2. from config.conf import cm
  3. HOST = 'HOST'
  4. class ReadConfig(object):
  5.     """配置文件"""
  6.     def __init__(self):
  7.         self.config = configparser.RawConfigParser()  # 当有%的符号时请使用Raw读取
  8.         self.config.read(cm.ini_file, encoding='utf-8')
  9.     def _get(self, section, option):
  10.         """获取"""
  11.         return self.config.get(section, option)
  12.     def _set(self, section, option, value):
  13.         """更新"""
  14.         self.config.set(section, option, value)
  15.         with open(cm.ini_file, 'w') as f:
  16.             self.config.write(f)
  17.     @property
  18.     def url(self):
  19.         return self._get(HOST, HOST)
  20. ini = ReadConfig()
  21. if __name__ == '__main__':
  22.     print(ini.url)
复制代码
4、记录操纵日志日志

在 utils 目次中新建 logger.py 文件。
  1. import logging
  2. from config.conf import cm
  3. class Log:
  4.     def __init__(self):
  5.         self.logger = logging.getLogger()
  6.         if not self.logger.handlers:
  7.             self.logger.setLevel(logging.DEBUG)
  8.             # 创建一个handle写入文件
  9.             fh = logging.FileHandler(cm.log_file, encoding='utf-8')
  10.             fh.setLevel(logging.INFO)
  11.             # 创建一个handle输出到控制台
  12.             ch = logging.StreamHandler()
  13.             ch.setLevel(logging.INFO)
  14.             # 定义输出的格式
  15.             formatter = logging.Formatter(self.fmt)
  16.             fh.setFormatter(formatter)
  17.             ch.setFormatter(formatter)
  18.             # 添加到handle
  19.             self.logger.addHandler(fh)
  20.             self.logger.addHandler(ch)
  21.     @property
  22.     def fmt(self):
  23.         return '%(levelname)s\t%(asctime)s\t[%(filename)s:%(lineno)d]\t%(message)s'
  24. log = Log().logger
  25. if __name__ == '__main__':
  26.     log.info('hello world')
复制代码
5、项目中元素相干

POM模子:PO模式详解
元素定位:①selenium UI自动化测试xpath定位详解 ;②selenium UI自动化测试之CSS元素定位语法详解
6、管理页面元素

1)新建page_element目次并在该目次下新建search.yaml文件
①本教程选择的测试地址是百度首页,以是对应的元素也是百度首页的。
②项目框架筹划中有一个page_element 目次就是专门来存放定位元素的文件的。
③通过对各种设置文件的对比,我在这里选择的是YAML文件格式。其易读,交互性好。
④在 page_element 中新建一个 search.yaml 文件。文件内容如下:
搜索框:"id==kw"
候选:"css==.bdsug-overflow"
搜索候选:"css==#form div li"
搜索按钮:"id==su"
2)在common目次中创建readelement.py文件。
①在 common 目次中创建 readelement.py 文件。实现了定位元素的存储和调用。文件内容如下:
②通过特别方法 getitem 实现调用恣意属性,读取yaml中的值。参考博客:python之__getitem__ 方法学习与利用 || python方法下划线定名规则
  1. import os
  2. import yaml
  3. from config.conf import cm
  4. class Element(object):
  5.     """获取元素"""
  6.     def __init__(self, name):
  7.         self.file_name = '%s.yaml' % name
  8.         self.element_path = os.path.join(cm.ELEMENT_PATH, self.file_name)
  9.         if not os.path.exists(self.element_path):
  10.             raise FileNotFoundError("%s 文件不存在!" % self.element_path)
  11.         with open(self.element_path, encoding='utf-8') as f:
  12.             self.data = yaml.safe_load(f)
  13.     def __getitem__(self, item):
  14.         """获取属性"""
  15.         data = self.data.get(item)
  16.         if data:
  17.             name, value = data.split('==')
  18.             return name, value
  19.         raise ArithmeticError("{}中不存在关键字:{}".format(self.file_name, item))
  20. if __name__ == '__main__':
  21.     search = Element('search')
  22.     print(search['搜索框'])
复制代码
3)新建script脚本文件目次并新建inspect.py文件
在 script 脚本文件目次中创建 inspect.py 文件,对全部的元素yaml文件内容举行检察。
  1. import os
  2. import yaml
  3. from config.conf import cm
  4. from utils.times import running_time
  5. @running_time
  6. def inspect_element():
  7.     """检查所有的元素是否正确
  8.     只能做一个简单的检查
  9.     """
  10.     for files in os.listdir(cm.ELEMENT_PATH):
  11.         _path = os.path.join(cm.ELEMENT_PATH, files)
  12.         with open(_path, encoding='utf-8') as f:
  13.             data = yaml.safe_load(f)
  14.         for k in data.values():
  15.             try:
  16.                 pattern, value = k.split('==')
  17.             except ValueError:
  18.                 raise Exception("元素表达式中没有`==`")
  19.             if pattern not in cm.LOCATE_MODE:
  20.                 raise Exception('%s中元素【%s】没有指定类型' % (_path, k))
  21.             elif pattern == 'xpath':
  22.                 assert '//' in value,\
  23.                     '%s中元素【%s】xpath类型与值不配' % (_path, k)
  24.             elif pattern == 'css':
  25.                 assert '//' not in value, \
  26.                     '%s中元素【%s]css类型与值不配' % (_path, k)
  27.             else:
  28.                 assert value, '%s中元素【%s】类型与值不匹配' % (_path, k)
  29. if __name__ == '__main__':
  30.     inspect_element()
复制代码
7、封装Selenium基类

①工厂模式的写法:很直白,简单,又明确。
【创建driver对象,打开百度网页,搜索selenium,点击搜索,然后停顿5秒,检察效果,末了关闭欣赏器。】
  1. import time
  2. from selenium import webdriver
  3. driver = webdriver.Chrome()
  4. driver.get('https://www.baidu.com')
  5. driver.find_element_by_xpath("//input[@id='kw']").send_keys('selenium')
  6. driver.find_element_by_xpath("//input[@id='su']").click()
  7. time.sleep(5)
  8. driver.quit()
复制代码
②那为什么要封装selenium的方法呢?
起首我们上述这种较为原始的方法,根本不实用于平时做UI自动化测试的:因为在UI界面实际运行环境远远比力复杂,大概因为网络缘故原由,大概控件缘故原由,我们元素还没有表现出来,就举行点击大概输入。
以是我们须要封装selenium方法,通过内置的显式等待或肯定的条件语句,才气构建一个稳固的方法。而且把selenium方法封装起来,有利于平时的代码维护。
新建page目次并创建webpage.py文件
①在 page 目次创建 webpage.py 文件。文件内容如下:
②在文件中我们对主要用了表现等待对selenium的 click , send_keys 等方法,做了二次封装。提高了运行的乐成率。
  1. from selenium.webdriver.support import expected_conditions as EC
  2. from selenium.webdriver.support.ui import WebDriverWait
  3. from selenium.common.exceptions import TimeoutException
  4. from config.conf import cm
  5. from utils.times import sleep
  6. from utils.logger import log
  7. """
  8. selenium基类
  9. 本文件存放了selenium基类的封装方法
  10. """
  11. class WebPage(object):
  12.     """selenium基类"""
  13.     def __init__(self, driver):
  14.         # self.driver = webdriver.Chrome()
  15.         self.driver = driver
  16.         self.timeout = 20
  17.         self.wait = WebDriverWait(self.driver, self.timeout)
  18.     def get_url(self, url):
  19.         """打开网址并验证"""
  20.         self.driver.maximize_window()
  21.         self.driver.set_page_load_timeout(60)
  22.         try:
  23.             self.driver.get(url)
  24.             self.driver.implicitly_wait(10)
  25.             log.info("打开网页:%s" % url)
  26.         except TimeoutException:
  27.             raise TimeoutException("打开%s超时请检查网络或网址服务器" % url)
  28.     @staticmethod
  29.     def element_locator(func, locator):
  30.         """元素定位器"""
  31.         name, value = locator
  32.         return func(cm.LOCATE_MODE[name], value)
  33.     def find_element(self, locator):
  34.         """寻找单个元素"""
  35.         return WebPage.element_locator(lambda *args: self.wait.until(
  36.             EC.presence_of_element_located(args)), locator)
  37.     def find_elements(self, locator):
  38.         """查找多个相同的元素"""
  39.         return WebPage.element_locator(lambda *args: self.wait.until(
  40.             EC.presence_of_all_elements_located(args)), locator)
  41.     def elements_num(self, locator):
  42.         """获取相同元素的个数"""
  43.         number = len(self.find_elements(locator))
  44.         log.info("相同元素:{}".format((locator, number)))
  45.         return number
  46.     def input_text(self, locator, txt):
  47.         """输入(输入前先清空)"""
  48.         sleep(0.5)
  49.         ele = self.find_element(locator)
  50.         ele.clear()
  51.         ele.send_keys(txt)
  52.         log.info("输入文本:{}".format(txt))
  53.     def is_click(self, locator):
  54.         """点击"""
  55.         self.find_element(locator).click()
  56.         sleep()
  57.         log.info("点击元素:{}".format(locator))
  58.     def element_text(self, locator):
  59.         """获取当前的text"""
  60.         _text = self.find_element(locator).text
  61.         log.info("获取文本:{}".format(_text))
  62.         return _text
  63.     @property
  64.     def get_source(self):
  65.         """获取页面源代码"""
  66.         return self.driver.page_source
  67.     def refresh(self):
  68.         """刷新页面F5"""
  69.         self.driver.refresh()
  70.         self.driver.implicitly_wait(30)
复制代码
8、创建页面临象

新建page_object目次并创建一个searchpage.py文件
①在 page_object 目次下创建一个 searchpage.py 文件。
②在该文件中我们对,输入搜索关键词,点击搜索,搜索遐想,举行了封装。【在平时中我们应该养成写解释的风俗,因为过一段时间后,没有解释,代码读起来很费劲。】
9、应用pytest测试框架

pytest.ini文件创建
①pytest项目中的设置文件,可以对pytest实验过程中操纵做全局控制。
②在项目根目次新建 pytest.ini 文件。文件内容如下:
  1. [pytest]
  2. addopts = --html=report.html --self-contained-html
复制代码
③addopts 指定实验时的其他参数分析:

  • –html=report/report.html --self-contained-html 天生pytest-html带样式的陈诉
  • -s 输出我们用例中的调式信息
  • -q 安静的举行测试
  • -v 可以输出用例更加详细的实验信息,比如用例地点的文件及用例名称等
10、编写测试用例

新建TestCase目次并创建test_search.py文件
①在 TestCase 目次中创建 test_search.py 文件。文件内容如下:
② pytest.fixture装饰器实现了和unittest的setup,teardown一样的前置启动,后置清算的装饰器。
③第一个测试用例:实现了在百度搜索selenium关键字,并点击搜索按钮,并在搜索效果中,用正则查找效果页源代码,返回数量大于10我们就认为通过。
④第二个测试用例:实现了百度搜索selenium关键字,然后断言搜索候选中的全部效果有没有selenium关键字。
  1. import re
  2. import pytest
  3. from utils.logger import log
  4. from common.readconfig import ini
  5. from page_object.searchpage import SearchPage
  6. class TestSearch:
  7.     @pytest.fixture(scope='function', autouse=True)
  8.     def open_baidu(self, drivers):
  9.         """打开百度"""
  10.         search = SearchPage(drivers)
  11.         search.get_url(ini.url)
  12.     def test_001(self, drivers):
  13.         """搜索"""
  14.         search = SearchPage(drivers)
  15.         search.input_search("selenium")
  16.         search.click_search()
  17.         result = re.search(r'selenium', search.get_source)
  18.         log.info(result)
  19.         assert result
  20.     def test_002(self, drivers):
  21.         """测试搜索候选"""
  22.         search = SearchPage(drivers)
  23.         search.input_search("selenium")
  24.         log.info(list(search.imagine))
  25.         assert all(["selenium" in i for i in search.imagine])
  26. if __name__ == '__main__':
  27.     pytest.main(['TestCase/test_search.py'])
复制代码
conftest.py
①在项目根目次下新建一个 conftest.py 文件。
②conftest.py是测试框架pytest的胶水文件,内里用到了fixture函数,封装并转达出了driver。
  1. import pytest
  2. from py.xml import html
  3. from selenium import webdriver
  4. driver = None
  5. @pytest.fixture(scope='session', autouse=True)
  6. def drivers(request):
  7.     """
  8.    
  9.     :param request: python内置的fixture函数,本函数中用来注册终结函数
  10.     :return: 返回driver实例
  11.     """
  12.     global driver
  13.     if driver is None:
  14.         driver = webdriver.Chrome()
  15.         driver.maximize_window()
  16.     def fn():
  17.         driver.quit()
  18.     request.addfinalizer(fn)
  19.     return driver
  20. @pytest.hookimpl(hookwrapper=True)
  21. def pytest_runtest_makereport(item):
  22.     """
  23.     当测试失败的时候,自动截图,展示到html报告中
  24.     :param item:
  25.     """
  26.     pytest_html = item.config.pluginmanager.getplugin('html')
  27.     outcome = yield
  28.     report = outcome.get_result()
  29.     report.description = str(item.function.__doc__)
  30.     extra = getattr(report, 'extra', [])
  31.     if report.when == 'call' or report.when == "setup":
  32.         xfail = hasattr(report, 'wasxfail')
  33.         if (report.skipped and xfail) or (report.failed and not xfail):
  34.             file_name = report.nodeid.replace("::", "_") + ".png"
  35.             screen_img = _capture_screenshot()
  36.             if file_name:
  37.                 html = '<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:1024px;height:768px;" ' \
  38.                        'οnclick="window.open(this.src)" align="right"/></div>' % screen_img
  39.                 extra.append(pytest_html.extras.html(html))
  40.         report.extra = extra
  41. def pytest_html_results_table_header(cells):
  42.     cells.insert(1, html.th('用例名称'))
  43.     cells.insert(2, html.th('Test_nodeid'))
  44.     cells.pop(2)
  45. def pytest_html_results_table_row(report, cells):
  46.     cells.insert(1, html.td(report.description))
  47.     cells.insert(2, html.td(report.nodeid))
  48.     cells.pop(2)
  49. def pytest_html_results_table_html(report, data):
  50.     if report.passed:
  51.         del data[:]
  52.         data.append(html.div('通过的用例未捕获日志输出.', class_='empty log'))
  53. def _capture_screenshot():
  54.     '''
  55.     截图保存为base64
  56.     :return:
  57.     '''
  58.     return driver.get_screenshot_as_base64()
复制代码
11、发送邮件

①当项目实验完成之后,须要发送到自己大概其他人邮箱里检察效果。
②编写发送邮件的模块。
③在 utils 目次中新建 send_mail.py 文件,文件内容如下:
  1. import zmail
  2. from config.conf import cm
  3. def send_report():
  4.     """发送报告"""
  5.     with open(cm.REPORT_FILE, encoding='utf-8') as f:
  6.         content_html = f.read()
  7.     try:
  8.         mail = {
  9.             'from': '[email protected]',
  10.             'subject': '最新的测试报告邮件',
  11.             'content_html': content_html,
  12.             'attachments': [cm.REPORT_FILE, ]
  13.         }
  14.         server = zmail.server(*cm.EMAIL_INFO.values())
  15.         server.send_mail(cm.ADDRESSEE, mail)
  16.         print("测试邮件发送成功!")
  17.     except Exception as e:
  18.         print("Error: 无法发送邮件,{}!", format(e))
  19. if __name__ == "__main__":
  20.     '''请先在config/conf.py文件设置QQ邮箱的账号和密码'''
  21.     send_report()
复制代码
④在 config/conf.py 文件中设置我们自己的QQ邮箱账号以及QQ邮箱授权码。运行 send_report() 函数。
末了感谢每一个认真阅读我文章的人,礼尚往来总是要有的,固然不是什么很值钱的东西,假如你用得到的话可以直接拿走:

这些资料,对于做【软件测试】的朋侪来说应该是最全面最完备的备战堆栈,这个堆栈也伴随我走过了最艰巨的旅程,盼望也能资助到你!凡事要趁早,特别是技能行业,肯定要提升技能功底。

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表