目录

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"},先写好,后面再进行方法编写调用类属性。