? ? ? ?web自动化测试,其实就是通过代码,模拟手工测试,进行的常规操作,从而达到解放劳动力、提高效率、节省成本,那么当我们用例多了,进行的操作多了,有什么办法可以提高效率呢,接下来就介绍下web的一些常用封装和操作。
一、前置条件
? ? ? ? 基于pytest测试框架的特性,前置条件可以怎么设置呢?我们可以通过把前置条件的部分代码,放置在一个叫 conftest.py 的文件中,如下:
from pages.pages_02_login import LoginPage
from selenium import webdriver
import pytest
# 启动浏览器的前置条件
@pytest.fixture()
def get_driver():
driver = webdriver.Chrome()
driver.implicitly_wait(10)
driver.maximize_window()
yield driver
driver.quit()
# 登录的前置条件
@pytest.fixture()
def teacher_login():
driver = webdriver.Chrome()
driver.implicitly_wait(10)
driver.maximize_window()
driver.get('https://xxxxxx.html')
login_page = LoginPage(driver)
login_page.login('lx@com','123456')
yield driver
driver.quit()
我们每次调用前置条件时,直接传入夹具函数名即可;
class TestCreatCourse:
# 调用login的前置条件
def test_close_creat_course(self,teacher_login):
driver = teacher_login
create_course = Create_classPage(driver)
create_course.loading().click_add_or_create_class().click_create_class().click_close() #点击创建/加入课程,点击创建课程
avatar_elem = create_course.find_element(create_course.actovar_locator)
try:
time.sleep(1)
assert avatar_elem.get_attribute('title') == 'xxx'
except AssertionError as e:
logger.error('测试用例不通过'.format(e))
raise e
class TestLogin:
# 调用get_driver的前置条件
def test_without_username_and_password(self,get_driver):
driver = get_driver
login_pages = LoginPage(driver)
login_pages.loading().login('','')
try:
actual = ['账号不能为空','密码不能为空']
assert login_pages.get_error_msgs() == actual
except AssertionError as e:
login_pages.screenshot()
logger.error('测试用例不通过:{}'.format(e))
raise e
另外:自动化测试优化流程
1、启动浏览器,条件反射天骄一隐形等待 ==> driver.implicity_wait(10)
2、最大化窗口, ==> driver.maximize_window()
3、PO模式:(下面详细描述)
- 把每一页的操作都封装成方法,(如登录、点击、获取文本等操作)
- 页面 url 优化成属性(url、__init__中的driver)
- 尽量让方法的返回值,返回一个pageobject对象,可为self,,也可为其他对象,(方便链式调用)
二、PO模式
== PO模式:(Page Object Model)PMO
把一个页面转化为对象,然后可通过调用,直接执行操作:
page.login() ==> 完成登录操作
page.clear() ==> 完成清除操作
page.load() ==> 完成页面访问操作
== 将web中的每个页面的操作,封装成一个类,
字面上的意思是;把浏览器的某页面(特征、操作)转化为代码(对象) == 》对象中有属性和方法
PO模式的本质:是对代码编写的一种封装
== PO模式有什么好处
1、可读性强 ==> 通过方法名称,表页面操作
2、可扩展性 ==> 可添加多个页面,且相对独立
3、可维护性 ==> 前端发生变化,用例发生变化,直接修改相应配置即可。
4、可复用性 ==> 重复代码可共用
== 页面操作中,返回(return)信息的操作,
- return self ==> 不需要返回数据,self表页面对象,以方便链式调用
- 需要获取数据时,返回数据,如:return text、 return actual_value
三、分层设计
同时结合分层设计思想,提取元素定位表达式,放置类属性下,以便当元素定位发生变化时,不用改动多条代码,举例如下:
====== login_page.py
class LoginPages:
host = 'xxx' # 通过路径拼接获取,配置在py文件或yaml文件当中,使用时需导入
url = host + '/Home/User/login.html'
username_locator = (By.NAME, 'account') # 用户名元素定位
password_locator = (By.NAME, 'pass') # 密码
login_btn_locator = (By.XPATH, "//div[contains(@class, 'pt-login')]/a[@class='btn-btn']") # 登录按钮
error_msg_locator = (By.CLASS_NAME, 'error-tips') # 错误信息弹框
def __init__(self,driver):
self.driver = driver
def login(self,account,password):
self.driver.find_elemet(*self.username_locator).send_keys(account) # 加上*,是因为参数为元组,有两个变量,要进行元组拆包
self.driver.find_elemet(*self.password_locator).send_keys(password)
self.driver.find_elemet(*self.login_btn_locator).click()
return self # 返回self,是方便链式调用
def get_url(self,url):
self.driver.get(self,url)
return self # 返回self,是方便链式调用
def get_error_msg(self):
elem = self.driver.find_elemet(*self.error_msg_locator)
text = [el.text for el in elem]
return text # 返回所需文本信息
执行用例,进行调用,是如何操作的呢,如下:
# 用例编写调用(导入loginpages)
class TestLogin:
def without_username_and_password(self,get_url): # get_url是conftest中的
driver = get_url
login_page = LoginPages(driver)
login_page.get_driver() # 此get_url为访问浏览器界面
actual = login_page.login('','').get_error_msg() # 链式调用
expect = ['账号不能为空','密码不能为空']
assert actual == expect
四、url 配置优化
== 配置信息可通过yaml文件、py文件、excel(但不推荐)
py文件直接导入模块就好
== yaml文件的话,需要读取yaml数据,并通过路径拼接获取数据(config中的path.py); 编写读取yaml文件的程序,是通用的
YAML文件写入:写入数据为:字段冒号后面要加一空格
如:host: "https://v4.ketangpai.com"
yaml文件读取编写如下:
# 读取yaml文件
def read_yaml(fpath):
with open(fpath,encoding = 'utf-8')as f:
data = yaml.safe_load(f)
return data
五、数据驱动
数据驱动 ==> 思想,
参数化 ==> 实现数据驱动的手段,
ddt ==> data-driver-testing 数据驱动测试
=== 数据驱动:
如有同一套代码,就只有数据不一样
就是当测试用例所有逻辑都一样,只有数据不同时,可把数据提取出来,
然后用数据去分别带入测试用例中
但是数据驱动不是万能的有些场景会不适用(如登录成功、登录失败,其断言验证的方式不一样,故无法进行ddt)
=== 数据驱动的好处:
- 实现了数据和代码分离,好维护
- 简化代码,不需重复编写函数名
=== 劣处:
- 数据驱动不方便调试 ==》解决方式:可先注释无异常的数据
- 如测试逻辑不通用,是不适合做数据驱动的
import pytest
data = [(1,1,2),(2,3,4)]
@pytest.mark.parametrize('info',data)
def test_add(info):
# assert info[0] + info[1] == info[2] # 方法一:通过索引的方式
param1,param2,expecct = info # 方式二,通过赋值的方式
assert param1 + param2 == expecct
@pytest.mark.parametrize('param1,param2,expecct',data) # 方法三:直接传入赋值变量
def test_demo(param1,param2,expecct):
assert param1 + param2 == expecct
六、BasePage
BasePage作用:
对浏览器的操作进行封装,是每个项目通用的,可在不同项目中使用
我们封装的basepage的方法越多,后面框架会越容易使用,是一个长期优化的过程。
封装:非常明显的信号是:对一段代码进行复制、粘贴时,
BasePage本质:
是对selenium的二次开发,
如cypress、playwright,当觉得框架不好用时,都可进行二次开发。
from datetime import datetime
import os
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
class BasePage:
host = ''
def __init__(self,driver):
self.driver = driver
def get(self,url):
self.driver.get(url)
if url.startswith('http'):
self.driver.get(url)
else:
url = self.host + url
self.driver.get(url)
return self
def click(self,locator,force = False): # 点击操作
elem = self.driver.find_element(*locator)
# 方法一:
# elem.click()
# 方法二:如果元素不可点击时,可使用:
if not force:
self.driver.execute_script("arguments[0].click()",elem)
else:
self.driver.execute_script(f"arguments[0].click(r'{force:true})",elem)
return self
def fill(self,locator,text): # 输入操作
elem = self.driver.find_element(*locator)
elem.send_keys(text)
return self
def get_attribute(self,locator,attr_name): # 获取某个元素的name属性,可随需要修改
elem = self.driver.find_element(*locator)
return elem.get_attribute(attr_name)
@property # 添加property后,表属性,后面调用不用加(),即elem.name即可
def name(self): # 获取name属性
return self.get_attribute('name')
def double_click(self,locator): # 上级操作
elem = self.driver.find_element(*locator)
action = ActionChains(self.driver)
action.double_click(elem).perform()
return self
def drag_and_drop(self,start_locator,end_locator): # 拖拽操作
start_elem = self.driver.find_element(*start_locator)
end_element = self.driver.find_element(*end_locator)
action = ActionChains(self.driver)
action.drag_and_drop(start_elem,end_element).perform()
return self
def move_to(self,locator): # 鼠标悬停
elem = self.driver.find_element(*locator)
action = ActionChains(self.driver)
action.move_to_element(elem).perform()
return self
def wait_visible(self,locator,timeout=10,poll=0.2): # 等待元素可见
wait = WebDriverWait(self.driver,timeout = timeout,poll_frequency=poll)
el = wait.until(expected_conditions.visibility_of_all_elements_located(locator)) #本身为元组参数,故无需拆包
return el
def wait_presence(self,locator,timeout=10,poll=0.2): # 等待元素出现
wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=poll)
el = wait.until(expected_conditions.presence_of_element_located(locator)) # 本身为元组参数,故无需拆包
return el
def wait_click(self,locator,timeout=10,poll=0.2): # 等待元素可点击
wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=poll)
el = wait.until(expected_conditions.element_to_be_clickable(locator)) # 本身为元组参数,故无需拆包
return el
def switch_to_frame(self,referce,timeout=10,poll=0.2): # iframe切换
# referce可为:name、id、element对象、索引、locator
if not referce:
return self.driver.switch_to.default_content() # 切回主界面
wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=poll)
el = wait.until(expected_conditions.frame_to_be_available_and_switch_to_it(referce)) # 本身为元组参数,故无需拆包
return el
def find_elements(self,locator):
return self.driver.find_elements(*locator)
def find_element(self,locator):
return self.driver.find_element(*locator)
def screenshot(self):
current_time = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
file_name = f'屏幕截图 - {current_time}。png'
file_path = os.path.join(path.img_page, file_name)
self.driver.get_screenshot_as_file(file_path)
return self
if __name__ == '__main__':
driver = webdriver.Chrome()
d = BasePage(driver)
d.get('http://www.baidu.com')
d.fill(('id','kw'),'haha')
d.click(('id','su'))
项目调用:
import random
import time
from selenium.webdriver.common.by import By
from common.Basepage import Basepage
from common.yaml_handler import info_data
class Create_classPage(Basepage):
host = info_data['host']
url = host + '/Main/index.html'
# 操作的元素定位
add_or_create_class_locator = ( By.XPATH,"//span[text()='+ 创建/加入课程']" )
create_class_locator = (By.XPATH,"//a[text()='创建课程']")
coures_name_input_locator = (By.XPATH,"//input[@placeholder='请输入课程名称']")
class_name_input_locator = (By.XPATH,"//input[@placeholder='请输入班级名称(选填)']")
creat_butto_locator = (By.XPATH,"//a[text()='创建']")
cancel_locator = (By.XPATH,"//div[@class='pop-btns']//a[text()='取消']")
close_locator = (By.XPATH,"//i[@class='close-newClass']")
# 断言元素表达式
actovar_locator = (By.XPATH,"//img[@title='xxx']")
def __init__(self,driver):
self.driver = driver
def loading(self):
self.driver.get(self.url)
return self
def click_add_or_create_class(self,):
self.click(self.add_or_create_class_locator)
return self
def click_create_class(self):
self.click(self.create_class_locator)
return self
def fill_coures_name(self,text):
self.send_keys(self.coures_name_input_locator,text)
return text
def fill_class_name(self,text):
self.send_keys(self.class_name_input_locator,text)
return text
def click_create(self):
self.click(self.creat_butto_locator)
return self
def click_cancel(self):
self.click(self.cancel_locator)
return self
def click_close(self):
self.click(self.close_locator)
return self
@staticmethod
def gen_class_name():
"""随机生成课程名称, 字母,生成10长度的字母。
aacdeyualp
"""
name = ''
for i in range(10):
letter = random.choice('abcdefghijklmnopqrstuvwxyz')
name += letter
return name + str(int(time.time()))
总结
以上仅做记录、方便复习,还不太全面,之后再继续优化把。
日常鸡汤:最近都在坚持运动,看自己能不能稍微瘦那么一点点,也控制饮食,看能不能有所不同吧,关键还是要:坚持坚持坚持,同样,学习也是如此,fighting!!~~
|