python+playwright+页面对象模型Page Object Models

一,项目目录:


image.png

二,安装环境

python3.7 以上
# 安装playwright
pip install playwright -i https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.tuna.tsinghua.edu.cn
playwright install
# 安装插件
pip install pytest
pip install pytest-playwright
pip install allure-pytest
pip install pytest-ordering

allure下载地址:https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/

三,代码实现

pytest.ini

[pytest]
log_cli = 1
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format = %Y-%m-%d %H:%M:%S
log_file = uitest.log
log_file_level = INFO
log_file_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_file_date_format=%Y-%m-%d %H:%M:%S

python_files = test_*.py  Test_*.py
python_classes = Test*
python_functions = test_* Test_*


# 设置默认执行命令
addopts = -v --alluredir=./report/xml --clean-alluredir --headed  --browser=webkit

ps:--base-url可添加baseurl, 如:--base-url=https://www.baidu.com/

conftest.py

import logging
import os
import time
import pytest
import allure
from playwright.sync_api import BrowserType
from typing import Dict


# 持久上下文,只打开一个上下文
@pytest.fixture(scope="session")
def context(
        browser_type: BrowserType,
        browser_type_launch_args: Dict,
        browser_context_args: Dict
):
    context = browser_type.launch_persistent_context('./auth', **{
        **browser_type_launch_args,
        **browser_context_args,
        "locale": "de-DE",
    })
    yield context
    context.close()


@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    """用例执行失败则添加失败截图至allure"""
    print('------------------------------------')
    # 获取钩子方法的调用结果
    outcome = yield
    rep = outcome.get_result()
    # 仅仅获取用例call 执行结果是失败的情况, 不包含 setup/teardown
    if rep.when == "call" and rep.failed:
        page = item.funcargs["page"]
        # 添加allure报告截图
        with allure.step('添加失败截图...'):
            allure.attach(page.screenshot(), "失败截图", allure.attachment_type.PNG)


def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """收集测试结果"""
    # print(terminalreporter.stats)
    total = terminalreporter._numcollected
    passed = len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown'])
    failed = len([i for i in terminalreporter.stats.get('failed', []) if i.when != 'teardown'])
    error = len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown'])
    skipped = len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown'])
    successful = len(terminalreporter.stats.get('passed', [])) / terminalreporter._numcollected * 100

    duration = time.time() - terminalreporter._sessionstarttime
    print('total times: %.2f' % duration, 'seconds')

    with open("result.txt", "w") as fp:
        fp.write(r"测试结果如下:" + "\n")
        fp.write("TOTAL=%s" % total + "\n")
        fp.write("PASSED=%s" % passed + "\n")
        fp.write("FAILED=%s" % failed + "\n")
        fp.write("ERROR=%s" % error + "\n")
        fp.write("SKIPPED=%s" % skipped + "\n")
        fp.write("SUCCESSFUL=%.2f%%" % successful + "\n")
        fp.write("TOTAL_TIMES=%.2fs" % duration)

BasePage.py

import logging
import os
from playwright.sync_api import Page, sync_playwright
from common.conf_yaml import get_conf, root_path
from common.log import setup_rotating_log

BaseUrl = get_conf("baseurl")["baseurl"]


class BasePage:
    _baseurl = ""
    logging = setup_rotating_log()

    def __init__(self, page=None):
        """
        :param page:
        :param on: 是否需要登陆
        """
        self.title = get_conf("title")["title"]
        self._state_file = os.path.join(root_path(), get_conf("state_file")["state_file"])
        if page is None:  # pytest-playwright 执行时不会从这里启动浏览器,这里是为了编写po对象时方便调试
            logging.info("page" + str(page))
            self.p = sync_playwright().start()
            self.browser = self.p.chromium.launch(headless=False)
            if os.path.isfile(self._state_file):
                self.context = self.browser.new_context(storage_state=self._state_file, base_url=BaseUrl)
            else:
                self.context = self.browser.new_context(base_url=BaseUrl)
            self.page: Page = self.context.new_page()
            logging.info(self._baseurl)
            self.page.goto(self._baseurl)
            if self.title not in self.page.title():
                self.login()
                logging.info("现在登陆成功了吗?%s" % (self.page.title()))
                self.context.storage_state(path=self._state_file)
        else:
            self.page: Page = page
        if not self.page.url.lower().startswith('http'):
            # logging.info(self._baseurl)
            self.page.goto(self._baseurl)
        if self.title not in self.page.title():
            self.login()
            logging.info("现在登陆成功了吗?%s" % (self.page.title()))
            # logging.info(self.page)

    def quit(self):
        try:
            self.context.storage_state(path=self._state_file)
            self.context.close()
            self.browser.close()
            self.p.stop()
        except Exception as e:
            print(e)

    def login(self):
        """
        登陆
        :return:
        """
        self.page.locator('#username').fill(get_conf("username")["username"])
        self.page.locator('#password').fill(get_conf("password")["password"])
        self.page.click('button.login-button')
        logging.info("进行登录")

if __name__ == '__main__':
    pg = BasePage()

页面Page(示例)

import logging
import os.path

from playwright.sync_api import expect

from common.conf_yaml import root_path
from page.BasePage import BasePage
from page.HealthDetailPage import HealthDetailPage


class HealthPage(BasePage):
    _baseurl = "/manage/health"

    def add_health(self, data):
        """
        点击接入健康拨测任务
        :param data: 接入拨测任务时需要输入的数据:{"jobName": "测试任务", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.2", "frequency": "5min",
            "point": ["顺德(内网)"], "remake": "添加健康拨测任务成功"}
        :return:
        """
        logging.info("添加拨测任务")
        self.page.click("button.ant-btn.ant-btn-primary.ant-btn-two-chinese-chars")
        self.page.wait_for_timeout(1000)
        self.page.locator("#jobName").fill(data['jobName'])
        if "systemId" in data and data["systemId"]:
            self.page.locator("#systemId").fill(data['systemId'])
            self.page.get_by_title(data['systemId']).click()
            if "env" in data and data["env"]:
                self.page.locator("#env").fill(data['env'])
                self.page.get_by_title(data['env']).click()
        if "probeType" in data and data["probeType"]:
            self.page.locator("#probeType").fill(data['probeType'])
            self.page.get_by_title(data['probeType']).click()
        self.page.locator("#url").fill(data['url'])
        if "frequency" in data and data["frequency"]:
            self.page.locator("#frequency").fill(data['frequency'])
            self.page.get_by_title(data['frequency'] + ' / 次', exact=True).click()
        if "point" in data and data["point"]:
            self.page.locator("#point").click()
            for e in data['point']:
                self.page.get_by_title(e).click()
        self.page.locator("#jobName").click()
        if "remake" in data and data["remake"]:
            self.page.locator("#remark").fill(data["remake"])
        self.page.locator("div.ant-space-item>button.ant-btn.ant-btn-primary>span").click()

    def add_health_fail(self, data):
        self.add_health(data)
        logging.info("添加拨测任务失败")
        ele_list = self.page.query_selector_all("div.ant-form-item-explain-error")
        errors = [e.text_content() for e in ele_list]
        self.page.get_by_role("button", name="取 消").click()
        return errors

    def get_health_names(self):
        """获取当前列表的监控名称列表"""
        logging.info("获取当前列表的监控名称列表")
        health_name_ele = "td.ant-table-cell.ant-table-cell-fix-left:nth-child(2)>div"
        self.page.wait_for_selector(health_name_ele)
        ele_list = self.page.query_selector_all(health_name_ele)
        # logging.info(ele_list)
        health_names = [e.text_content() for e in ele_list]
        logging.info(health_names)
        return health_names

    def get_health_address(self):
        """获取当前列表的拨测地址列表"""
        logging.info("获取当前列表的拨测地址列表")
        health_address_ele = "td.ant-table-cell.ant-table-cell-fix-left:nth-child(3)"
        self.page.wait_for_selector(health_address_ele)
        ele_list = self.page.query_selector_all(health_address_ele)
        # logging.info(ele_list)
        health_addresses = [e.text_content() for e in ele_list]
        logging.info(health_addresses)
        return health_addresses

    def goto_health_details(self, data):
        """
        点击进入对应健康拨测详情
        :param data:
        :return:
        """
        self.search_health(data)
        logging.info("进入对应健康拨测详情")
        self.redo()
        self.page.wait_for_timeout(1000)
        self.page.get_by_text("详情", exact=True).click()
        return HealthDetailPage(self.page)


if __name__ == '__main__':
    data = {"jobName": "测试任务", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.2",
            "frequency": "5min",
            "point": ["顺德(内网)"]}
    hp.add_health(data)
   

用例放在testcase文件夹下:

import logging
import allure
import pytest
from playwright.sync_api import Page

from page.HeathPage import HealthPage


@allure.feature("健康拨测模块")
@allure.story("测试健康拨测模块")
class Test_HealthCheck:

    @pytest.fixture(autouse=True)
    def setup(self, page: Page):
        self.page = page
        yield
        self.page.close()

    @pytest.mark.run(order=0)
    @allure.title("用例标题:test_add_health_success")
    @pytest.mark.parametrize('data', [
        {"jobName": "UI自动化测试任务", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.2",
         "frequency": "5min", "point": ["顺德(内网)"], "status": "下发中"}])
    def test_add_health_success(self, data):
        """
        添加拨测任务成功
        :param data: 用例数据
        :return:
        """
        healthpage = HealthPage(self.page)
        with allure.step("1.添加健康拨测任务成功;2.点击刷新;3.验证添加信息匹配(断言拨测名称,拨测地址,添加成功任务状态为下发中)"):
            healthpage.add_health(data)
            healthpage.redo()
            assert data['jobName'] in healthpage.get_health_names()
            assert data['url'] in healthpage.get_health_address()
            assert data['status'] in healthpage.get_health_status()

    # @pytest.mark.skip
    @pytest.mark.run(order=2)
    @allure.title("用例标题:test_add_health_fail")
    @pytest.mark.parametrize('data', [
        {"jobName": "", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "point": ["顺德(内网)"], "error": "监控名称不能为空"},
        {"jobName": "UI自动化测试任务", "systemId": "", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "point": ["顺德(内网)"], "error": "归属系统不能为空"},
        {"jobName": "UI自动化测试任务", "systemId": "T-mPTS", "env": "", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "point": ["顺德(内网)"], "error": "环境不能为空"},
        {"jobName": "UI自动化测试任务", "systemId": "T-mPTS", "env": "SIT", "probeType": "", "url": "127.0.0.1",
         "frequency": "5min", "point": ["顺德(内网)"], "error": "拨测类型不能为空"},
        {"jobName": "UI自动化测试任务", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "",
         "frequency": "5min", "point": ["顺德(内网)"], "error": "例如:10.1.1.1或onemonitorsit.midea.com"},
        {"jobName": "UI自动化测试任务", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "error": "拨测点不能为空"}
    ])
    def test_add_health_fail(self, data):
        """
        验证添加用例健康拨测失败场景
        :param data:
        :return:
        """
        healthpage = HealthPage(self.page)
        with allure.step("1.添加健康拨测任务失败;2.提示验证"):
            assert data['error'] in healthpage.add_health_fail(data)

common/log.py

import logging
import logging.handlers
from common import conf_yaml
import os


def setup_rotating_log(log_file="uitest.log"):
    """用于调试页面page时打印日志"""
    log_path_file = os.path.join(conf_yaml.root_path(), log_file)
    # 定义日志格式
    log_formatter = logging.Formatter('%(asctime)s [%(levelname)6s] %(message)s (%(filename)s:%(lineno)s)',
                                      datefmt='%Y-%m-%d %H:%M:%S')

    max_log_size = 1 * 1024 * 1024 * 1024  # 1G
    backup_count = 2

    # 创建滚动文件处理器
    rotating_handler = logging.handlers.RotatingFileHandler(
        filename=log_path_file, maxBytes=max_log_size, backupCount=backup_count)
    rotating_handler.setFormatter(log_formatter)

    # 创建控制台处理器
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(log_formatter)

    # 创建日志记录器
    logger = logging.getLogger()
    logger.addHandler(rotating_handler)  # 添加滚动文件处理器
    logger.addHandler(console_handler)  # 添加控制台处理器
    logger.setLevel(logging.INFO)

    return logger






common/conf_yaml.py

import os
import yaml

def get_conf(*key):
    """
    获取配置
    :param key:需要获取的配置名称
    :return:
    """
    my_conf = os.path.join(root_path(), r'auth\my_conf.yml')
    # print(my_conf)
    with open(my_conf, encoding='utf-8') as f:
        res = yaml.load(f.read(), Loader=yaml.FullLoader)
        res_dict = {}
        for e in key:
            if e in res:
                res_dict[e] = res[e]
        return res_dict


def root_path():
    """返回工程主路径"""
    if str(os.path.dirname(os.path.realpath(__file__))).endswith('common'):
        path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
    else:
        path = os.path.dirname(os.path.realpath(__file__))
    # print(path)
    return path

配置文件放在auth下:

image.png

四,执行用例后查看
执行结果:


image.png

image.png

五,查看allure测试报告:


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359