当前位置:   article > 正文

UI自动化测试:pytest+selenium+allure+po模式实现_pytest+po 做ui自动化

pytest+po 做ui自动化

菜鸡自用,避免又一次失忆,找不到东南西北。

在网上搜刮各位测试大佬的文章,终于搭起来基本的po模型框架

不得不说,脚本跑通带来的自豪感真是棒极了O(∩_∩)O哈哈~

其实脚本中还有很多问题,但是能够读懂里面的代码,并且应用到脚本中,进步不少,继续加油!

如果觉得还可以的话,麻烦给新人一个小小的赞吧~~~

1、Common

base_page:公共元素封装

定义driver初始化、查找元素、点击、输入、错误截图等一些方法;值得一提的是,我在错误截图的方法中加入了allure,当生成错误截图时,会把这个截图保存进allure报告,并且名称与路径一致

  1. # encoding: utf-8
  2. from selenium.webdriver.remote.webdriver import WebDriver
  3. from selenium import webdriver
  4. from selenium.webdriver.support.wait import WebDriverWait
  5. from selenium.webdriver.support import expected_conditions as EC
  6. from Common.log_handle import logger
  7. from Config.path_name import PathName
  8. from Common import time
  9. import allure
  10. class BasePage:
  11. # 定义类变量driver和base_url
  12. _driver = None
  13. _base_url = "https://test.com"
  14. def __init__(self, driver: WebDriver = None):
  15. # 判断是否传入了driver,如果初次调用,则初始化driver
  16. if driver is None:
  17. self._driver = webdriver.Chrome()
  18. self._driver.maximize_window()
  19. self._driver.implicitly_wait(3)
  20. # 非首次调用,传入了driver,则把传入的driver赋给_driver
  21. else:
  22. self._driver = driver
  23. # 判断base_url是否为空,非空则调用get进行打开操作
  24. if self._base_url != "":
  25. self._driver.get(self._base_url)
  26. def find(self, by, locator):
  27. return self._driver.find_element(by, locator)
  28. # 定义可被点击判断的方法
  29. def wait_for_click(self, locator, time=10):
  30. WebDriverWait(self._driver, time).until(EC.element_to_be_clickable(locator))
  31. # 二次封装元素等待,如果错误,记录日志,并截图保存
  32. def wait(self, loc, filename):
  33. """
  34. 元素等待
  35. :param loc: 等待的元素
  36. :param filename: 截图名字
  37. :return:
  38. 这里使用的是隐式等待,同时将隐式等待和元素是否可见的判断进行了结合,这样更加稳定!
  39. """
  40. logger.info('{}正待等待元素{}'.format(filename, loc))
  41. try:
  42. WebDriverWait(self._driver, timeout=30).until(EC.visibility_of_element_located(loc))
  43. #首先是隐式等待表达式(driver对象,等待时长)
  44. #同时每0.5秒会查看一次,查看元素是否出现,如果超过30s未出现,则报错timeout
  45. #until()是等待元素可见,这里加入了元素是否可见的判断
  46. except Exception as e:
  47. self.error_screenshots(filename)
  48. logger.exception('元素等待错误发生:{}元素为{}'.format(e, loc))
  49. raise
  50. def error_screenshots(self, name):
  51. """
  52. 保存截图,并将对应截图上传至allure报告,图片名称和文件路径一致
  53. :param name:根据被调用传入的名字,生成png的图片
  54. :return:
  55. """
  56. try:
  57. file_path = PathName.screenshots_path
  58. times = time.time_sj()
  59. filename = file_path + times + '{}.png'.format(name)
  60. self._driver.get_screenshot_as_file(filename)
  61. logger.info("正在保存图片:{}".format(filename))
  62. allure.attach(self._driver.get_screenshot_as_png(), attachment_type=allure.attachment_type.PNG,name=filename)
  63. except Exception as e:
  64. logger.error('图片报存错误:{}'.format(e))
  65. raise
  66. def get_ele(self, loc, filename):
  67. """
  68. 查找元素
  69. :param loc:
  70. :param filename:
  71. :return:
  72. """
  73. logger.info('{}正在查找元素:{}'.format(filename, loc))
  74. try:
  75. # 这里使用的是find_element查找单个元素,这里需要传入的是一个表达式,需要告诉driver对象使用的是什么定位方法,以及元素定位!
  76. # By是继承了selenium里面的8大定位方法,所以框架里操作元素的皆是By.XPATH或者By.id等等
  77. # 同时因为需要传入的是一个表达式,而By.XPATH是一个元组,这里做了解包处理
  78. ele = self._driver.find_element(*loc)
  79. except Exception as e:
  80. logger.exception('查找元素失败:')
  81. self.error_screenshots(filename)
  82. raise
  83. else:
  84. return ele
  85. def send_key(self, loc, name, filename):
  86. """
  87. 输入文本
  88. :param loc:元素
  89. :param filename:截图名字
  90. :param name: 输入的名字
  91. :return:
  92. """
  93. logger.info('{}正在操作元素{},输入文本{}'.format(filename, loc, name))
  94. self.wait(loc, filename)
  95. try:
  96. self.get_ele(loc, filename).send_keys(name)
  97. except:
  98. logger.exception('元素错误 {}:')
  99. self.error_screenshots(filename)
  100. raise
  101. def click_key(self, loc, filename):
  102. """
  103. 元素点击
  104. :param loc:
  105. :param filename:
  106. :return:
  107. """
  108. logger.info('{}正在操作元素{}'.format(filename, loc))
  109. self.wait(loc, filename)
  110. try:
  111. self.get_ele(loc, filename).click()
  112. except Exception as e:
  113. logger.exception('点击元素错误:{}'.format(e))
  114. self.error_screenshots(filename)
  115. raise
  116. def get_ele_text(self, loc, filename):
  117. """
  118. 获取元素文本
  119. :param loc:
  120. :param filename:
  121. :return:
  122. """""
  123. logger.info('{}正在获取文本{}'.format(filename, loc))
  124. self.wait(loc, filename)
  125. ele = self.get_ele(loc, filename)
  126. try:
  127. text = ele.text
  128. logger.info('获取文本成功{}'.format(text))
  129. return text
  130. except:
  131. logger.exception('获取文本错误:')
  132. self.error_screenshots(filename)
  133. def get_ele_attribute(self, loc, attribute_name, filename):
  134. """
  135. 获取元素属性
  136. :param loc:
  137. :param attribute_name:
  138. :param filename:
  139. :return:
  140. """
  141. logger.info('{}正在获取元素{}的属性'.format(filename, loc))
  142. self.wait(loc, filename)
  143. ele = self.get_ele(loc, filename)
  144. try:
  145. value = ele.get_attribute(attribute_name)
  146. logger.info('获取属性成功{}'.format(value))
  147. return value
  148. except:
  149. logger.exception('获取属性失败')
  150. self.error_screenshots(filename)
  151. def wait_ele_click(self, loc, filename):
  152. logger.info('{}正待等待可点击元素{}'.format(filename, loc))
  153. try:
  154. WebDriverWait(self._driver, timeout=20).until(EC.element_to_be_clickable(loc))
  155. # logger.info('等待可点击元素{}'.format(loc))
  156. except:
  157. self.error_screenshots(filename)
  158. logger.exception('等待可点击元素错误:元素为{}'.format(loc))
  159. raise
  160. def switch_to_iframe(self, loc, filename):
  161. try:
  162. WebDriverWait(self._driver, 20).until(EC.frame_to_be_available_and_switch_to_it(loc))
  163. logger.info('正在进入嵌套页面:{}'.format(loc))
  164. except:
  165. logger.exception('进入嵌套页面失败{}'.format(loc))
  166. self.error_screenshots(filename)
  167. def click_wait_ele(self, loc, filename):
  168. logger.info('正在等待{}中的可点击元素出现{}'.format(filename, loc))
  169. self.wait_ele_click(loc, filename)
  170. try:
  171. self.get_ele(loc, filename).click()
  172. logger.info('正在{}中点击元素{}'.format(filename, loc))
  173. except:
  174. logger.info('在{}当中点击{}元素失败'.format(filename, loc))
  175. self.error_screenshots(filename)

log_handle:日志封装

  1. # encoding: utf-8
  2. """
  3. 封装log日志模块
  4. """
  5. import logging
  6. import os
  7. from Config.common_data import Context
  8. from Config.path_name import PathName
  9. log_path_name = os.path.join(PathName.logs_path, Context.log_name)
  10. # print(log_path_name)
  11. class LogHandel(logging.Logger):
  12. """定义日志类"""
  13. def __init__(self, name, file, level='DEBUG',
  14. fmt="%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s"):
  15. super().__init__(name)
  16. self.setLevel(level)
  17. file_headers = logging.FileHandler(file)
  18. file_headers.setLevel(level)
  19. self.addHandler(file_headers)
  20. fmt = logging.Formatter(fmt)
  21. file_headers.setFormatter(fmt)
  22. logger = LogHandel(Context.log_name, log_path_name, level=Context.level)
  23. if __name__ == '__main__':
  24. log = logger
  25. log.warning('测试1')

time:生成时间

  1. # encoding: utf-8
  2. """
  3. 生成时间
  4. """
  5. import datetime
  6. def time_sj():
  7. sj = datetime.datetime.now().strftime('%m-%d-%H-%M-%S')
  8. return sj

2、Config

common_data 日志等级

  1. # encoding: utf-8
  2. """
  3. 日志等级
  4. """
  5. class Context:
  6. log_name = 'log打印.txt'
  7. level = 'DEBUG'

path_name 存放路径

  1. # encoding: utf-8
  2. """
  3. 存放路径
  4. """
  5. import os
  6. class PathName:
  7. # 初始路径
  8. dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  9. #
  10. options_path = os.path.join(dir_path, 'Options')
  11. # Logs文件目录地址
  12. logs_path = os.path.join(options_path, 'Logs/')
  13. # report测试报告路径
  14. report_path = os.path.join(options_path, 'report/')
  15. # 错误截图路径
  16. screenshots_path = os.path.join(options_path, 'error_screenshots/')
  17. # 测试用例路径
  18. case_name = os.path.join(dir_path, 'testcases/')

3、Options 文件存放文件夹

在path_name定义的脚本中生成的log、report和错误截图存放路径,在后面的代码中也确实可以在对应的文件夹下面查看到生成的相关数据

4、page 页面元素定位

login

登录页面所有元素定位,直接通过F12复制生成的,后面还需要优化

  1. # encoding: utf-8
  2. """
  3. 登录页面元素定位
  4. """
  5. from selenium.webdriver.common.by import By
  6. from selenium.webdriver.remote.webdriver import WebDriver
  7. from Common.log_handle import logger
  8. from Common import base_page
  9. class BackGroundLoc(base_page.BasePage):
  10. # 登录窗口
  11. input_user = (By.XPATH, '//*[@id="app"]/div/div/form/div[1]/div/div[1]/input')
  12. # 输入密码
  13. input_code = (By.XPATH, '//*[@id="app"]/div/div/form/div[2]/div/div/div/input')
  14. # 用户登录
  15. user_login = (By.XPATH, '//*[@id="app"]/div/div/form/button')
  16. # 页面名称
  17. title_name = '服务平台'
  18. # error_msg
  19. error_msg = (By.CSS_SELECTOR, '.el-message.el-message--error')

5、PageObject

login_home 登录页面

首先调用BasePage里面的方法将元素内容进行设置,并传入一个描述信息,在后期测试用例中直接引用就好

  1. #!/usr/bin/python3
  2. # coding=gbk
  3. import time
  4. from page.login import BackGroundLoc as BLC
  5. from Common.base_page import BasePage
  6. from selenium.webdriver.remote.webdriver import WebDriver
  7. class Login(BasePage):
  8. # 输入账号密码
  9. def platform_login_succeed(self, phone, code):
  10. self.send_key(BLC.input_user, phone, '登录页_输入用户名')
  11. self.send_key(BLC.input_code, code, '登录页_输入密码')
  12. time.sleep(2)
  13. # 登录
  14. def login(self):
  15. self.click_key(BLC.user_login, '登录页_点击登陆')
  16. time.sleep(2)
  17. def login_fail(self):
  18. error_text = self.get_ele_text(BLC.error_msg,filename='login_fail')
  19. assert error_text == "验证码错误", f"实际错误信息为:{error_text}"

6、test_cases

conftest 前后置条件

  1. # encoding: utf-8
  2. """
  3. pytest前后置条件
  4. """
  5. from Common.base_page import BasePage as GD
  6. from Common.log_handle import logger
  7. import pytest
  8. from selenium import webdriver
  9. # @pytest.fixture装饰器,是表示接下来的函数为测试用例的前后置条件
  10. #fixture一共4个级别,默认scope=function(用例,对标为unittest中的setup以及tearDown)
  11. #同时,fixture中包含了前置条件,以及后置条件
  12. #function是每个用例开始和结束执行
  13. #class是类等级,每个测试类执行一次
  14. #modules 是模块级别,也就是当前.py文件执行一次
  15. #session 是会话级别, 指测试会话中所有用例只执行一次
  16. #pytest中区分前置后置是用关键字yield来区分的, yield之前,是前置!yield之后,是后置 yield同行,是返回数据
  17. @pytest.fixture()
  18. def open_browser():
  19. logger.info('-----------正在执行测试用例开始的准备工作,打开浏览器,请求后台-----------')
  20. driver = webdriver.Chrome()
  21. driver.get(GD._base_url)
  22. driver.maximize_window()
  23. yield driver
  24. driver.quit()

run_all 运行

清空测试报告文件夹,运行用例并生成allure报告至前面定义的路径下

  1. import os
  2. import pytest
  3. import shutil
  4. from Config.path_name import PathName
  5. """运行测试用例并生成allure报告"""
  6. if __name__ == '__main__':
  7. # 清空之前的报告文件夹
  8. if os.path.exists(PathName.report_path):
  9. shutil.rmtree(PathName.report_path)
  10. os.makedirs(PathName.report_path)
  11. # 运行test02测试用例,如果要运行文件夹下面所有的测试用例,则把文件名改为./文件夹名
  12. pytest.main(['test_login.py', "-sv", "--alluredir", PathName.report_path+"json"])
  13. # 生成报告,存储在path_name指定的文件夹下面
  14. os.system("allure generate {} -o {} --clean".
  15. format(PathName.report_path + "json", PathName.report_path + "html"))
  16. # 生成报告,存储在当前文件夹下面
  17. # os.system("allure generate ./report/json -o ./report/html --clean")

test_login 登录测试用例

  1. # encoding: utf-8
  2. from Common.log_handle import logger
  3. from PageObject.login_home import Login
  4. from Test_Data.test_data import TestData as TD
  5. import pytest,allure
  6. import time
  7. data = TD.test_data
  8. @allure.epic('服务平台')
  9. @allure.feature('登录页面')
  10. # pytest.mark.usefixtures()是使用前置条件,括号中填写要使用的前置条件函数名称
  11. # 因为封装前置条件的py文件名称固定,所以这里不需要导入,是由pytest自动遍历查找
  12. # 放在类方法上,是表示测试类下所有测试方法,都会使用这个前置条件
  13. @pytest.mark.usefixtures('open_browser') # 用此方法可以提前打开网页
  14. class Test_heart_login:
  15. """
  16. # #正向场景
  17. # 3.输入账号密码,点击登陆
  18. # 4.选择对应子平台
  19. # 6.判断首页元素是否可见
  20. # :return:
  21. # """
  22. @allure.story('[case01] 登录成功')
  23. def test_login_success(self,open_browser):
  24. try:
  25. with allure.step('打开页面'):
  26. lg = Login(open_browser)
  27. with allure.step('输入手机号、验证码登录'):
  28. lg.platform_login_succeed(TD.just_data['phone'], TD.just_data['code'])
  29. time.sleep(1)
  30. with allure.step('点击登录按钮'):
  31. lg.login()
  32. logger.info('正在执行测试用例:手机号{},验证码{}'.format(TD.just_data['phone'], TD.just_data['code']))
  33. except Exception as e:
  34. logger.error('用例执行错误:{}'.format(e))
  35. raise AssertionError
  36. """
  37. @pytest.mark.parametrize()装饰器是用来执行数据驱动的,需要传入两个值
  38. 1.数据名称,不固定,自由填写
  39. 2.数据
  40. 但是数据名称要在使用数据驱动的方法里当作参数传入,对标ddt当中的传值接收
  41. """
  42. @allure.story('[case02] 登录失败')
  43. @pytest.mark.parametrize('test_data', TD.test_data)
  44. def test_login_02_fail(self, open_browser, test_data):
  45. """
  46. 3.输入账号密码
  47. 4.选择对应平台
  48. 5.点击登陆
  49. 6.查看失败提示
  50. :return:
  51. """
  52. logger.info('+++++正在执行逆向向登陆测试用例+++++')
  53. try:
  54. with allure.step('打开页面'):
  55. pf = Login(open_browser)
  56. with allure.step('输入手机号、验证码登录'):
  57. pf.platform_login_succeed(test_data['phone'], test_data['code'])
  58. time.sleep(1)
  59. with allure.step('点击登录按钮'):
  60. pf.login()
  61. pf.login_fail()
  62. pf.error_screenshots('login_fail')
  63. logger.info('正在执行逆向场景用例用户名{},密码{}'.format(test_data['phone'], test_data['code']))
  64. except Exception as e:
  65. logger.error('逆向场景用件执行失败{}'.format(e))

7、Test_Data 测试数据

test_data测试数据

  1. # encoding: utf-8
  2. """
  3. 测试数据
  4. """
  5. class TestData:
  6. # 正确账号密码
  7. just_data = {
  8. 'phone': '1234567899',
  9. 'code': '123456'
  10. }
  11. # 错误测试数据
  12. test_data = [{'phone': '12345678999', 'code': '116611'},
  13. {'phone': '12345678999', 'code': '116612'}]

8、运行脚本后,访问本地allure报告

页面中的测试项目、模块、步骤都是在测试用例中标记的节点

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/911112
推荐阅读
相关标签
  

闽ICP备14008679号