目录
81节
1.PageObject页面对象
2.数据分组
3.locator分层
1.PageObject页面对象
1)将某个网页(或者APP页面)封装成对象
对象:
--属性:比如元素定位器、标题、URL(参考DOM)
--方法:比如元素定位、点击、鼠标拖拽 (动作、操作就是方法)
上篇博客中,完成LoginPage类的封装,这个类其实就是个页面,在这个页面下有某些行为(操作),某些行为可以是一步操作,也可以是多步操作的集合.
比如:类里面的login方法,是一个行为,里面又包含多个行为:定位元素-->键盘输入数据(又属于定位元素的子动作),定位元素-->点击登录按钮
那么思考一个问题:上面的login行为下面的多个行为,需不需要单独再进行封装??
PageObject设计原则:
是不是所有的动作都需要单独封装成页面对象的方法?-------------不必要
①因为有的动作是用不到的,比如单独定位元素又不进行用户输入,这种场景是不存在的。(定位元素的目的就是进行用户操作的)
②页面中有些行为是个整体:比如输入用户名、密码,点击登录,是一系列整体动作;
如果输入用户名跟密码是分开的,可以将这2步操作进行单独封装再调用,比如:
原则:根据项目中实际应用,用到什么行为,就封装什么方法
分层思想:便于维护(前端修改了代码,只需要修改少量代码即可)
2)PageObject有什么好处?---面试
①可维护性
比如:一个页面,前端工程师修改了元素定位方式,那么就不需要修改test_login.py,直接找到pages文件下面对应修改的某个页面,修改对应的元素定位方式即可。
其他的文件不需要改动。如果不用PO模式,只要用到这个页面的元素定位,就要修改(检查很多文件),代码管理差、可维护性差。
又或者测试用例需要重新设计,那么页面对象模型里面的代码就不需要修改。--可维护性高
②扩展性
当有新需求新功能,实现更加方便
比如:前端新增新的页面功能,原来的功能不需要改,直接在PO模型页面对象pages中去封装新的功能即可
③可读性
PO模式下,比如LoginPage(driver).login(.....),看到这个,就能理解是登录页面的登录操作,(本身就具有注释功能),易理解。
如果PO模式下,命名无法实现见名知意,可以在封装的函数中添加详细的注释(文档字符串)介绍(但是在代码调用的时候,很少写注释的,只是在封装的函数中写详细的注释):比如
④可复用性 重复使用(封装函数、类本来就是为了提高可复用性)
⑤页面操作 和 测试操作的分离(√)
3)依据PO模式,对pages中的login.py中的LoginPage类进行分层设计:
class LoginPage: """登录""" #初始化driver def __init__(self,driver): self.driver = driver def login(self,username,password): # 访问登录页面 url = "http://120.78.128.25:8765/Index/login.html" self.driver.get(url) # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element_by_class_name("btn-special").click() def enter_username(self,username): "输入用户名" self.driver.find_element_by_name("phone").send_keys(username) def enter_password(self,password): "输入密码" self.driver.find_element_by_name("password").send_keys(password) def get_error_info(self): "获取登录失败的错误信息" return self.driver.find_element_by_class_name("form-error-info").text
test_login.py:
import pytest from middware.handler import HandlerMiddle from middware.pages.login import LoginPage #获取excel中login数据 data = HandlerMiddle.excel.read_data("login") class TestLogin(): """登录功能的测试类""" @pytest.mark.error @pytest.mark.parametrize("test_info",data) def test_login_error(self,test_info,driver): """登陆失败用例""" #初始化 操作的页面 对象 login_page = LoginPage(driver) #测试步骤:输入用户名、密码、登录(调用po中的方法) login_page.login(username=eval(test_info["data"])["username"], password=eval(test_info["data"])["password"]) #获取登录失败的错误信息 actual_result =login_page.get_error_info() # 断言 expected_result = test_info["expected_result"] assert actual_result in expected_result
由上总结,测试用例(比如:test_login_error(self):)的编程步骤基本都是下面3个步骤:
--1.初始化页面操作
--2.调用页面逻辑操作,来获取实际结果
--3.实际结果跟预期结果比对,即断言
4)链式调用
上面的test_login.py调用LoginPage中的方法,调用了2次,如下:
在实际项目,会出现同时调用很多个方法,那么在test_login.py就要一直写调用的语句,比较麻烦,因此可以采取链式调用的方式,简化调用的过程。
①链式调用,被调用的函数的返回值(没有跳转页面)需要是它自己self
修改login.py中的方法,如下:
"""登录页面""" class LoginPage: """登录""" #初始化driver def __init__(self,driver): self.driver = driver def login(self,username,password): # 访问登录页面 url = "http://120.78.128.25:8765/Index/login.html" self.driver.get(url) # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element_by_class_name("btn-special").click() return self def enter_username(self,username): "输入用户名" self.driver.find_element_by_name("phone").send_keys(username) return self def enter_password(self,password): "输入密码" self.driver.find_element_by_name("password").send_keys(password) return self def get_error_info(self): "获取登录失败的错误信息" return self.driver.find_element_by_class_name("form-error-info").text
注意上面获取登录失败,页面显示的错误信息时,返回值是text文本,不是self.<-------进行断言
②test_login.py进行链式调用:
"""登录功能的测试用例""" import pytest from middware.handler import HandlerMiddle from middware.pages.login import LoginPage #获取excel中login数据 data = HandlerMiddle.excel.read_data("login") class TestLogin: """登录功能的测试类""" @pytest.mark.error @pytest.mark.parametrize("test_info",data) def test_login_error(self,test_info,driver): """登陆失败用例""" #初始化 操作的页面 对象 login_page = LoginPage(driver) #测试步骤:输入用户名、密码、登录(调用po中的方法) actual_result =login_page.login(username=eval(test_info["data"])["username"], password=eval(test_info["data"])["password"]).get_error_info() # 断言 expected_result = test_info["expected_result"] assert actual_result in expected_result
上面的链式调用,表示,先调用login()方法(调用完以后返回的是self),再调用get_error_info()方法
PO设计原则更新:
PageObject设计原则:
1)是不是所有的动作都需要单独封装成页面对象的方法?----不必要
①因为有的动作是用不到的,比如单独定位元素又不进行用户输入,这种场景是不存在的。(定位元素的目的就是进行用户操作的)
②页面中有些行为是个整体:比如输入用户名、密码,点击登录,是一系列整体动;
2)封装的页面操作的返回值 (如上面的链式调用的使用)
①执行完一步操作还停留在原来的页面,返回值就是self
执行完一步操作跳转到别的页面,返回值是:其他页面的对象
②如果需要获取某个元素或者属性,就直接返回元素本身、属性的值
③如果一个操作可能有多个返回结果(self / 跳转别的页面):根据结果封装成多个方法。
例如:对于LoginPage中的login()方法,如果没有登录成功,返回的就是self,---->封装成login_fail()方法
登录成功,发生页面跳转,返回的就是别的页面对象。----->login_success()方法
以上完成了对login_error()测试用例的分层,那么对于login_success(),,基本PO原则中的3)-③,进行进一步的封装完善。
5)登录成功用例
思想梳理:在测试用例login_success()中第一步是要初始化页面,但是需要初始化2个登录的页面(如上面的LoginPage()对象),还需要封装一个登录成功后跳转的页面
在pages文件中封装一个index.py,为登录成功的
①封装indexpage
"""登录成功页面""" class IndexPage: """登录成功""" #初始化driver def __init__(self,driver): self.driver = driver #获取登录成功的用户名 def get_account_name(self): web_element=self.driver.find_element_by_xpath('//a[@href="/Member/index.html"]') return web_element.text
②login.py中再封装一个登录成功的方法
"""登录页面""" from middware.pages.index import IndexPage class LoginPage: """登录""" #初始化driver def __init__(self,driver): self.driver = driver def login_fail(self,username,password): # 访问登录页面 url = "http://120.78.128.25:8765/Index/login.html" self.driver.get(url) # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element_by_class_name("btn-special").click() return self def login_success(self,username,password): # 访问登录页面 url = "http://120.78.128.25:8765/Index/login.html" self.driver.get(url) # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element_by_class_name("btn-special").click() return IndexPage(self.driver) def enter_username(self,username): "输入用户名" self.driver.find_element_by_name("phone").send_keys(username) return self def enter_password(self,password): "输入密码" self.driver.find_element_by_name("password").send_keys(password) return self def get_error_info(self): "获取登录失败的错误信息" return self.driver.find_element_by_class_name("form-error-info").text
login_success()方法返回的是跳转的页面index_page()
③完成TestLogin中:login_success()测试用例
"""登录功能的测试用例""" import pytest from middware.handler import HandlerMiddle from middware.pages.login import LoginPage from middware.pages.index import IndexPage #获取excel中login数据 data = HandlerMiddle.excel.read_data("login") class TestLogin: """登录功能的测试类""" @pytest.mark.error @pytest.mark.parametrize("test_info",data) def test_login_error(self,test_info,driver): """登陆失败用例""" #初始化 操作的页面 对象 login_page = LoginPage(driver) #测试步骤:输入用户名、密码、登录(调用po中的方法) actual_result =login_page.login_fail(username=eval(test_info["data"])["username"], password=eval(test_info["data"])["password"]).get_error_info() # 断言 expected_result = test_info["expected_result"] assert actual_result in expected_result @pytest.mark.success @pytest.mark.parametrize("test_info",[{"username":"159********","password":"15","expected_result":"我的帐户[小蜜蜂177872141]"}]) def test_login_success(self,test_info,driver): """登录成功测试用例""" #初始化页面对象 login_page = LoginPage(driver) index_page = IndexPage(driver) #执行测试,获取实际结果, login_page.login_success(username=eval(test_info["data"])["username"], password=eval(test_info["data"])["password"]) actual_result = index_page.get_account_name() #断言 assert actual_result in test_info["expected_result"]
有上面的【 login_success()方法返回的是跳转的页面index_page()】,可知PO模式的另一个优势:无需多次初始化页面对象,直接进行链式调用即可。如下:
测试用例中的login_page对象调用的login_success()方法的返回值就是IndexPage对象,行链式调用:
"""登录功能的测试用例""" import pytest from middware.handler import HandlerMiddle from middware.pages.login import LoginPage#获取excel中login数据 data = HandlerMiddle.excel.read_data("login") class TestLogin: """登录功能的测试类""" @pytest.mark.error @pytest.mark.parametrize("test_info",data) def test_login_error(self,test_info,driver): """登陆失败用例""" #初始化 操作的页面 对象 login_page = LoginPage(driver) #测试步骤:输入用户名、密码、登录(调用po中的方法) actual_result =login_page.login_fail(username=eval(test_info["data"])["username"], password=eval(test_info["data"])["password"]).get_error_info() # 断言 expected_result = test_info["expected_result"] assert actual_result in expected_result @pytest.mark.success @pytest.mark.parametrize("test_info",[{"username":"15955100283","password":"Cj900815","expected_result":"我的帐户[小蜜蜂177872141]"}]) def test_login_success(self,test_info,driver): """登录成功测试用例""" #初始化页面对象 login_page = LoginPage(driver) #执行测试,获取实际结果, actual_result = login_page.login_success(username=test_info["username"], password=test_info["password"]).get_account_name() #断言 assert actual_result in test_info["expected_result"]
2.数据分组
①分组的依据:测试步骤不一致
只要是测试步骤不一样,就一定要进行数据分组
②测试用例的管理:(最好不要用Excel去管理,在这里用excel管理反而比较复杂:比如读取出来是json格式还要进行转化)
---1.py文件
---2.yaml文件,支持字典格式
下面用py文件进行测试用例的管理:data目录下-->login_data.py
"""登录测试用例数据""" #登录失败数据 data_error = [ {"username":"","password":"","expected_results":"请输入手机号"}, {"username":"123","password":"","expected_results":"请输入正确的手机号"} ] #登录成功用例 data_success = [ {"username": "15955100283", "password": "Cj900815", "expected_results": "我的帐户[小蜜蜂177872141]"} ]
再次修改test_login()测试用例,并添加log信息(直接调用接口的logging_handler中的方法):
"""登录功能的测试用例""" import pytest from middware.pages.login import LoginPage from data.login_data import data_error,data_success from middware.handler import HandlerMiddle #获取excel中login数据 # data = HandlerMiddle.excel.read_data("login") @pytest.mark.login class TestLogin: """登录功能的测试类""" @pytest.mark.error @pytest.mark.parametrize("test_info",data_error) def test_login_error(self,test_info,driver): """登陆失败用例""" #初始化 操作的页面 对象 login_page = LoginPage(driver) #测试步骤:输入用户名、密码、登录(调用po中的方法) actual_result =login_page.login_fail(username=test_info["username"], password=test_info["password"]).get_error_info() # 断言 expected_result = test_info["expected_results"] try: assert actual_result in expected_result except AssertionError as e: HandlerMiddle.logger.error("测试用例username为{},不通过!".format(test_info["username"])) raise e @pytest.mark.success @pytest.mark.parametrize("test_info",data_success) def test_login_success(self,test_info,driver): """登录成功测试用例""" #初始化页面对象 login_page = LoginPage(driver) #执行测试,获取实际结果, actual_result = login_page.login_success(username=test_info["username"], password=test_info["password"]).get_account_name() #断言 try: assert actual_result in test_info["expected_results"] except AssertionError as e: HandlerMiddle.logger.error("测试用例username为{},不通过!".format(test_info["username"])) raise e
运行通过!
当然,你也可以用yaml文件去管理测试用例的数据,调用yaml数据即可,自行尝试。
3.locator分层
1)优化URL
login.py中的URL还可以进一步优化
# 访问登录页面 url = "http://120.78.128.25:8765/Index/login.html" self.driver.get(url)
将域名host放在yaml配置文件中管理,将URL作为类属性。(因为不同的项目的域名不一样,直接放在yaml中管理,维护方便,可复用性高)
其次:将访问页面单独进行封装,如下login.py:
"""登录页面""" from middware.pages.index import IndexPage from middware.handler import HandlerMiddle class LoginPage: """登录"""
URL =HandlerMiddle.yaml_data["host"] + "/Index/login.html" #初始化driver def __init__(self,driver): self.driver = driver def get(self): """访问页面""" self.driver.get(self.URL) return self def login_fail(self,username,password): # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element_by_class_name("btn-special").click() return self def login_success(self,username,password): # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element_by_class_name("btn-special").click() return IndexPage(self.driver) def enter_username(self,username): "输入用户名" self.driver.find_element_by_name("phone").send_keys(username) return self def enter_password(self,password): "输入密码" self.driver.find_element_by_name("password").send_keys(password) return self def get_error_info(self): "获取登录失败的错误信息" return self.driver.find_element_by_class_name("form-error-info").text
在test_login.py用例中,调用login中的方法就要添加get(),完善链式调用:
actual_result =login_page.get().login_fail(username=test_info["username"], password=test_info["password"]).get_error_info()
2)优化元素定位
对于 self.driver.find_element_by_class_name("btn-special").click()中,前端修改了元素表达方式的话,维护起来会非常麻烦,所以要进行优化。
那么,将元素定位locators,提取出来应该放在那里管理?
①locators是跟页面LoginPage绑定在一起的,是该页面的属性,所以可以提取出来作为类属性;-----提取:定位的方式,以及数值(因为定位的方式也可能会发生变化),元祖 、字典的形式表示
②find_element()方法,源码中的2个参数为:by,value,即定位的方式,和值。调用locator的元祖()表示方式)),用*args进行元祖解包;调用locator的字典{}表示方式,用**args进行字典解包
举例1:元祖表示
name:表示元素定位的方式(By)
btn-special:表示name 的值,元素定位的属性(value)
login_btn_locator = ("name","btn-special")
调用解包:
def login_fail(self,username,password): # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element(*self.login_btn_locator).click() return self
举例2:字典表示
login_btn_locator = {"by":"name","value":"btn-special"}
调用:
self.driver.find_element(*self.login_btn_locator).click()
由上面的例子,将所有的元素定位进行优化,lgin.py:(注意类属性的元素定位,By为class name时,中间没有下划线)
"""登录页面""" from middware.pages.index import IndexPage from middware.handler import HandlerMiddle class LoginPage: """登录""" URL =HandlerMiddle.yaml_data["host"] + "/Index/login.html" #登录按钮,元祖形式 #login_btn_locator = ("name","btn-special") #登录按钮 login_btn_locator = {"by":"class name","value":"btn-special"} #用户名 username_locator = {"by":"name","value":"phone"} #密码 password_locator = {"by":"name","value":"password"} #登陆失败的错误信息 error_msg_locator = {"by":"class name","value":"form-error-info"} #初始化driver def __init__(self,driver): self.driver = driver def get(self): """访问页面""" self.driver.get(self.URL) return self def login_fail(self,username,password): # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) #self.driver.find_element(*self.login_btn_locator).click() self.driver.find_element(**self.login_btn_locator).click()#点击登录按钮 return self def login_success(self,username,password): # 元素定位+元素操作,输入用户名和密码,点击登录进行提交 self.enter_username(username) self.enter_password(password) self.driver.find_element(**self.login_btn_locator).click()#点击登录按钮 return IndexPage(self.driver) def enter_username(self,username): "输入用户名" self.driver.find_element(**self.username_locator).send_keys(username) return self def enter_password(self,password): "输入密码" self.driver.find_element(**self.password_locator).send_keys(password) return self def get_error_info(self): "获取登录失败的错误信息" return self.driver.find_element(**self.error_msg_locator).text
提示:在实际项目中,进行元素定位时,就可以先将PO模式中的类属性,比如 username_locator = {"by":"name","value":"phone"},先写好,后面再进行方法编写调用类属性。