跳转至

POM

Page Objects Model,Page Objects 是 Web 应用程序 UI 的简单抽象,是自动化页面测试中流行的一种设计模式,最早由 Martin Fowler 提出,被 Selenium 所接受并推广。

https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/

目的:页面分层,松耦合

原则

  • 测试用例中尽量不要暴露页面的内部结构,可以将页面提供的服务或组件封装到公共方法中,比如 xxx_page,但没必要封装整个页面,也可以是某个功能模块
po/
    base/:存放selenium和appium最基本的方法,其它页面继承base复用__init__(),避免重复初始化
    app/: 存放appium初始化相关的配置
    web/: 存放selenium初始化相关的配置
  • xxx_page 中同一操作的不同结果最好建模为不同的方法
  • 实例化页面对象时,最好先判断一下页面的关键元素是否已正确加载
  • 断言是测试的一部分,应始终在测试代码中,即 xxx_page 中一般不做断言
  • xxx_page 中返回 yyy_page,形成链式调用,使得逻辑跳转清晰,另外最好使用局部导入,避免循环引用
# 调用其它页面
def goto other_page(self):
    return OtherPage(self.driver)

# 调用自己
def goto_self(self):
    return self

测试实践

base_page.py

import os

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class BasePage:
    _browser = os.getenv("browser", "chrome")
    _url = ""  # 通常是index页面

    def __init__(self, driver_base=None):
        if driver_base:
            # 链式调用时,复用self.driver,不重复初始化
            self.driver = driver_base
        else:
            if self._browser == "safari":
                self.driver = webdriver.Safari()
            else:
                options = webdriver.ChromeOptions()
                # 去除Chrome正在受到自动软件的控制的提示
                options.add_experimental_option('useAutomationExtension', False)
                options.add_experimental_option('excludeSwitches', ['enable-automation'])
                # 关闭是否保存密码弹框
                prefs = {"credentials_enable_service": False, "profile.password_manager_enabled": False}
                options.add_experimental_option("prefs", prefs)
                # 仅加载完DOM后即可开始定位
                options.page_load_strategy = 'eager'
                # 运行完不自动关闭窗口
                # options.add_experimental_option("detach", True)
                self.driver = webdriver.Chrome(options=options)
            self.driver.maximize_window()
            self.driver.get(self._url)


    def find_input(self, locator):
        element = WebDriverWait(self.driver, 5).until(
            EC.presence_of_element_located(locator)
        )
        return element


    def find_button(self, locator):
        element = WebDriverWait(self.driver, 5).until(
            EC.element_to_be_clickable(locator)
        )
        return element


    def find_select(self, locator):
        pass        

login_page.py

import time
from selenium.webdriver.common.by import By
from po.base_page import BasePage


class LoginPage(BasePage):
    _agree_button = (By.XPATH, "")
    _login_button = (By.XPATH, "")
    _email_input = (By.CSS_SELECTOR, "")
    _password_input = (By.CSS_SELECTOR, "")


    def login(self, url, email, password):
        # 打开登陆页面
        self.driver.get(url)

        # 点击我同意
        agree_button = self.find_button(self._agree_button)
        agree_button.click()

        # 输入邮箱
        email_input = self.find_input(self._email_input)
        email_input.send_keys(email)

        # 输入密码
        password_input = self.find_input(self._password_input)
        password_input.send_keys(password)

        # 点击登录
        login_button = self.find_button(self._login_button)
        login_button.click()
        time.sleep(3)  # 强制3s等待页面加载


    def logout(self):
        pass


    # 跳转其它页面
    def goto_kyc_page(self):
        from po.kyc_page import KYCPage  # 局部引用
        return KYCPage(self.driver)  # 复用driver

登陆如果不是被测试对象,最好不要使用UI自动化的方式实现,而是采用接口登陆,然后设置浏览器Cookies或Token保持登陆,减少页面加载和等待时间

# 接口登陆
response = requests.post(login_url, data=login_data)
cookies = response.cookies  # <RequestsCookieJar[<Cookie csrftoken=4NKYzMJ9...b8MzTTJfwF3n for example.com/>, <Cookie sessionid=bq6r4ycioahmt...d2di0srnv1 for example.com/>]>
token = response.json().get('token')
  • 设置 Cookies
driver.get('https://example.com')  # 打开网站,确保URL匹配Cookies的域
# 需要将Cookeis对象转换为Dict格式
cookies_dict = {
    'name': cookie.name,
    'value': cookie.value,
    'path': '/',
    'domain': cookie.domain
}
for cookie in cookies:
    driver.add_cookie(cookies_dict)
driver.get('https://example.com')  # 再次访问网站,以便使用新的登录状态
  • 设置 Token

Token 通常是存储在 Local Storage 或 Session Storage 或设置在请求头中

# localStorage和sessionStorage是按照同源策略进行隔离的,这意味着只能在设置了这些数据的相同域名下访问
# 在设置token前访问一次,确保JavaScript代码在正确的域上下文中运行它们
driver.get('https://example.com')

# 通过 JavaScript 在 Local Storage 中设置 token
driver.execute_script("window.localStorage.setItem('token', '{}');".format(token))
# 或者设置到 Session Storage
driver.execute_script("window.sessionStorage.setItem('token', '{}');".format(token))


# 刷新或导航,以便使用新的登录状态
driver.get('https://example.com')
  • 修改请求头

Selenium WebDriver API 本身不支持直接修改 HTTP 请求头,如果必须要在请求头中携带 token 或其它字段,可以考虑使用代理服务器(如BrowserMob Proxy)来拦截和修改发往服务器的HTTP请求。大致步骤:下载并启动 BrowserMob Proxy,配置 Selenium 使用 BrowserMob Proxy 作为 HTTP 代理,使用 BrowserMob Proxy 的 API 来添加或修改请求头。

xxx_test.py

import pytest

type_list = ["id", "passport"]

class TestKYC:
    def setup_class(cls):
        user_dict = {"email": "test@demo.com", "password": "xxx"}
        from po.login_page import LoginPage
        cls.login_page = LoginPage()
        # self.login_page.login(user_dict)
        cls.login_page.login_by_api(user_dict)

    @pytest.mark.parametrize("type", type_list)
    def test_submit_kyc(self, type):
        kyc_page = self.login_page.goto_kyc_page()
        kyc_page.fill_document(type)
        kyc_page.fill_profile()
        kyc_page.fill_address()
        assert True

如果测试不同type需要登陆不同的email,可以使用 setup + fixture 的方式

import pytest


test_data = [
    ('test1@demo.com', 'id'),
    ("test2@demo.com", 'passport')
]


class TestKYC:
    # 注意,scope范围要用function
    @pytest.fixture(scope="function", params=test_data, autouse=True)
    def setup_class(self, request):
        email, document_type = request.param
        user_dict = {"email": email, "password": "xxx"}
        from po.login_page import LoginPage
        self.login_page = LoginPage()
        self.login_page.login_by_api(user_dict)
        return document_type

    def test_submit_kyc(self, setup_class):
        document_type = setup_class
        kyc_page = self.login_page.goto_kyc_page()
        kyc_page.fill_document(document_type)
        kyc_page.fill_profile()
        kyc_page.fill_address()
        assert True