PO设计模式

一、什么是PO设计模式?

PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个 Page 类,即一个py文件,并以页面为单位来写测试用例,实现页面对象和测试用例的分离,若元素发生变化,只需要进入对应的Page类,更新元素定位即可,不用修改用例。

二、在什么样的场景下使用PO设计模式?(where)

随着时间的推移,需要维护的页面越来越多时,如果使用传统的设计模式把测不同页面上的所有步骤写在同一个模块里面,会显得笨重。如果页面发生了改变,对应的脚本也要发生改变,难以维护就可以体现出来了,所以使用的PO设计模式会方便很多。

三、PO模式的六大原则

(1)一个 public 方法代表一个公共的服务。就是说一个方法代替页面上的某个操作(公共方法表示页面提供的服务)
(2)PageObject 中的方法细节不可暴露在外,通过提供公共服务接口的形式提供给外部(不要暴露页面的细节)
(3)一般不需要在 PageObject 中断言(Page设计中不要出现断言,应该写在测试用例类中)
(4)当有页面跳转的操作时候,执行这个方法时应该在方法结束返回时能够跳转到另一个页面中(方法应该返回其他的Page对象)
(5)我们只需要对页面中我们需要的重要的内容进行封装(不要去代表整个page,如果一个页面中有很多功能,只需要对重点功能封装方法即可)
(6)页面中相同的组件,但是不同的操作应该要被拆成不同的方法进行封装(不同的结果返回不同的方法,不同的模式)

四、PO 模式可以把一个页面分为二个层级:对象操作层、业务层。

(1)对象操作层:封装定位元素,封装对元素的操作。
(2)业务层:将一个或多个操作组合起来完成一个业务功能。

五、以下是以企业微信-添加成员为例-最终打印成员列表

传统的web自动化脚本
(1)传统的web脚本,把driver实例化、对象操作以及业务逻辑全部写进一个模块里面;这次举例的测试用例只是涉及到两个页面,如果遇到到更加复杂的测试用例时,把全部的代码写进同一个模块里,这样就显得很臃肿;当有上百个用例,几十个页面的时候,我们会在测试用例中重复的使用到页面当中的元素和操作。当其中的页面发生变化时,我们需要在多个用例中去修改。这种情况下,代码多且乱,维护成本也不低。
(2)使用PO设计模式编写测试用例时,通过分层将测试代码和页面元素分开,有效的命名模块名称可以更加清晰知道所操作功能模块以及操作的UI元素,哪个页面的数据发生改变时,就在对应的页面进行修改即可;

'''测试用例'''
class TestDemo:
    def setup(self):
        self.driver = webdriver.Chrome()
        self.driver.implicitly_wait(5)
        self.driver.maximize_window()

    def teardown(self):
        self.driver.quit()

    def test_add_mumber(self):
        # 扫码登录界面
        self.driver.get("https://work.weixin.qq.com/wework_admin/loginpage_wx")
        # 创建data.yaml文件,设置编码
        path = os.path.dirname(os.path.abspath(__file__))
        data_file = os.sep.join([path, "../data/data.yaml"])
        with open(data_file, encoding="UTF-8") as f:
            # 获取数据
            yaml_data = yaml.safe_load(f)
            # 遍历
            for cookie in yaml_data:
                # 设置cookie
                self.driver.add_cookie(cookie)
        # 访问首页
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame")
        #点击添加成员
        self.driver.find_element_by_xpath("//*[@id='_hmt_click']/div[1]/div[4]/div[2]/a[1]/div/span[2]").click()
        time.sleep(3)
        #输入用户名
        self.driver.find_element_by_id("username").send_keys("haha")
        #输入别名
        self.driver.find_element_by_id("memberAdd_english_name").send_keys("哈哈哈")
        #输入账号
        self.driver.find_element_by_id("memberAdd_acctid").send_keys("haha001")
        # self.driver.find_element_by_xpath("//*[@id='js_contacts72']/div/div[2]/div/div[4]/div/form/div[2]/div[1]/div[3]/div[2]/label[2]/input").click()
        #选择性别为“女”
        self.driver.find_element_by_css_selector("#js_contacts72 > div > div.member_colRight > div > div:nth-child(4) > div > form > div.member_edit_formWrap > div:nth-child(1) > div.member_edit_item.member_edit_item_Radios > div.member_edit_item_right > label:nth-child(2) > input").click()
          #输入手机号码
        self.driver.find_element_by_id("memberAdd_phone").send_keys("15676006789")
        #输入座机
        self.driver.find_element_by_id("memberAdd_telephone").send_keys("0770851")
        #输入邮箱
        self.driver.find_element_by_id("memberAdd_mail").send_keys("1234@qq.com")
        #输入地址
        self.driver.find_element_by_id("memberEdit_address").send_keys("xxx")
        #点击保存
        self.driver.find_element_by_css_selector("#js_contacts72 > div > div.member_colRight > div > div:nth-child(4) > div > form > div:nth-child(3) > a.qui_btn.ww_btn.js_btn_save").click()
        time.sleep(2)


PO项目目录结构:

(1)项目开始前,拿到一份需求时,需要做需求分析,划分模块,编写测试用例。此次测试用例的流程,进入主页(MainPage),点击添加成员,进入添加成员页面,点击添加,点击保存,显示所有的成员列表,最后做断言,验证测试用例执行是否成功。
(2)编写测试用例开始之前,建立时序图,便于思考,时序图如下图所示:

(3)测试用例的要素:
前置条件
执行步骤
数据检查及断言

'''
    添加成员测试用例  
'''
class TestAddMumber:
    def setup(self):
        # 实例化
        self.main_page = MainPage()
    # 实现测试数据和页面对象分离
    @pytest.mark.parametrize("username,accid,phone", [("hxc2", "862", "15676000002")])
    def test_add_mumber(self, username, accid, phone):
        # 1.跳转到添加成员页面  2.添加成员   3.获取成员列表
        name_list = self.main_page.goto_add_number().add_member(username, accid, phone).get_contact_list()
        assert username in name_list

(2)main_page.py该模块就是相当于该测试的首页,该成员页面有添加成员、导入通讯录、打卡等功能,以下以点击“添加成员”为例,点击“添加成员”之后进入到通讯录页面,添加成员所在的页面和通讯录不在同一个页面上,当有页面跳转的操作时候,在函数后面需要返回要跳转页面的实例化对象,如下图代码中的AddMumberPage。

class MainPage(BasePage):
    '''使用公共方法代表UI所提供的功能'''
    def goto_add_number(self):

        #点击添加成员
        self.driver.find_element(By.CSS_SELECTOR,".ww_indexImg_AddMember").click()
        '''
        跳转到添加成员页面
        :return:
        '''
        #返回要跳转页面的实例化对象
        return AddMumberPage(self.driver)

(3)添加成员,在该模块中定义add_mumber接口,把定位元素以及对应的操作封装在add_member里,使用时调用add_mumber接口即可。PO中的方法细节不可暴露在外,通过提供公共服务接口的形式提供给外部
#调用时要注意格式(元组),self.driver.find_element需要*号将元组解包。

class AddMumberPage(BasePage):
    # 设定为元祖
    #__是私有,页面元素不需要让业务了解,所以要加私有
    #元素定位
    __ele_username = (By.ID, "username")
    ele_accid = (By.ID, "memberAdd_acctid")
    ele_phone = (By.ID, "memberAdd_phone")
    webdriver.Firefox()
    #元素对象
    def add_member(self, username, accid, phone):
        '''
        *是解元祖,self.driver.find_element(*self.ele_username)等同于self.driver.find_element(By.ID, "username")
        '''
        # 填写用户名
        self.find(*self.__ele_username).send_keys(username)
        # 填写账号
        self.driver.find_element(*self.ele_accid).send_keys(accid)
        # 填写手机号
        self.driver.find_element(*self.ele_phone).send_keys(phone)
        # 点击保存
        self.driver.find_element(By.CSS_SELECTOR, ".js_btn_save").click()
        '''
        页面的return分成两个部分
        1.其他页面的实例
        2.用例所需要的断言
        注意:不要写成ContactPage,这个是类
        :return:
        '''
        # 返回通讯实例对象
        return ContactPage(self.driver)

(4)获取成员列表

class ContactPage(BasePage):
    #通讯录列表
    def get_contact_list(self):
        ele_list = self.driver.find_elements(By.CSS_SELECTOR,".member_colRight_memberTable_td:nth-child(2)")
        print(ele_list)
        name_list = []
        #遍历元素列表,通过元素的text属性,提取文本数据信息
        for ele in ele_list:
            name_list.append(ele.text)
        print(name_list)
        return name_list

(5)基础层base_page.py文件,该模块主要是要把页面重复的步骤抽离出来,比如driver的实例化。

class BasePage:
    '''
    driver重复实例化,导致页面启动多次
    解决driver重复实例化问题
    '''

    # 没有参数传入,会去默认参数None,如果有参数就选去传入的参数
    # 如果 driver 存在, 不为None,就复用这个driver 
    # 如果 driver 不存在(为None),就重新创建一个新的driver
    def __init__(self, base_driver=None):
        if base_driver == None:
            # 实例化driver
            self.driver = webdriver.Chrome()
            # 扫码登录界面
            self.driver.get("https://work.weixin.qq.com/wework_admin/loginpage_wx")
            # 创建data.yaml文件,设置编码
            path = os.path.dirname(os.path.abspath(__file__))
            data_file = os.sep.join([path, "../../data/data.yaml"])
            with open(data_file, encoding="UTF-8") as f:
                # 获取数据
                yaml_data = yaml.safe_load(f)
                # 遍历
                for cookie in yaml_data:
                    # 设置cookie
                    self.driver.add_cookie(cookie)
            #刷新页面
            self.driver.refresh()
            # 访问首页
            # self.driver.get("https://work.weixin.qq.com/wework_admin/frame#index")
            self.driver.implicitly_wait(3)
        else:
            self.driver = base_driver

    #封装定位元素
    def find(self,by,ele = None):
        """
        :param by:定位方式,css,xpath,id
        :param ele:元素定位方式
        """
        #传入两种定位元素,提高代码的兼容性
        if ele == None:
            return self.driver.find_element(*by)
        else:
            return self.driver.find_element(by,ele)

总结:
(1)通过页面分层,将测试代码和被测试页面的页面元素及其操作方法进行分离,代码可阅读性增强,整体流程更为清晰,降低代码冗余。
(2)页面对象与用例分离,业务代码与测试代码分离,降低耦合性。
(3)不同层级分属不同用途,降低维护成本。
(4)更加有效的命名方式使得我们更加清晰的知道方法所操作的UI元素,例如我们要回到首页,方法命名为:MainPage(),通过方法名即可清晰的知道具体的功能实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橙子软件测试菇凉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值