python+playwright+頁(yè)面對(duì)象模型Page Object Models

一摧玫,項(xiàng)目目錄:


image.png

二筐摘,安裝環(huán)境

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/

三,代碼實(shí)現(xiàn)

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_*


# 設(shè)置默認(rèn)執(zhí)行命令
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


# 持久上下文拇砰,只打開(kāi)一個(gè)上下文
@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):
    """用例執(zhí)行失敗則添加失敗截圖至allure"""
    print('------------------------------------')
    # 獲取鉤子方法的調(diào)用結(jié)果
    outcome = yield
    rep = outcome.get_result()
    # 僅僅獲取用例call 執(zhí)行結(jié)果是失敗的情況, 不包含 setup/teardown
    if rep.when == "call" and rep.failed:
        page = item.funcargs["page"]
        # 添加allure報(bào)告截圖
        with allure.step('添加失敗截圖...'):
            allure.attach(page.screenshot(), "失敗截圖", allure.attachment_type.PNG)


def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """收集測(cè)試結(jié)果"""
    # 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"測(cè)試結(jié)果如下:" + "\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 執(zhí)行時(shí)不會(huì)從這里啟動(dòng)瀏覽器,這里是為了編寫(xiě)po對(duì)象時(shí)方便調(diào)試
            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("現(xiàn)在登陸成功了嗎狰腌?%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("現(xiàn)在登陸成功了嗎除破?%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("進(jìn)行登錄")

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

頁(yè)面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):
        """
        點(diǎn)擊接入健康撥測(cè)任務(wù)
        :param data: 接入撥測(cè)任務(wù)時(shí)需要輸入的數(shù)據(jù):{"jobName": "測(cè)試任務(wù)", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.2", "frequency": "5min",
            "point": ["順德(內(nèi)網(wǎng))"], "remake": "添加健康撥測(cè)任務(wù)成功"}
        :return:
        """
        logging.info("添加撥測(cè)任務(wù)")
        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("添加撥測(cè)任務(wù)失敗")
        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):
        """獲取當(dāng)前列表的監(jiān)控名稱(chēng)列表"""
        logging.info("獲取當(dāng)前列表的監(jiān)控名稱(chēng)列表")
        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):
        """獲取當(dāng)前列表的撥測(cè)地址列表"""
        logging.info("獲取當(dāng)前列表的撥測(cè)地址列表")
        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):
        """
        點(diǎn)擊進(jìn)入對(duì)應(yīng)健康撥測(cè)詳情
        :param data:
        :return:
        """
        self.search_health(data)
        logging.info("進(jìn)入對(duì)應(yīng)健康撥測(cè)詳情")
        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": "測(cè)試任務(wù)", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.2",
            "frequency": "5min",
            "point": ["順德(內(nèi)網(wǎng))"]}
    hp.add_health(data)
   

用例放在testcase文件夾下:

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

from page.HeathPage import HealthPage


@allure.feature("健康撥測(cè)模塊")
@allure.story("測(cè)試健康撥測(cè)模塊")
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("用例標(biāo)題:test_add_health_success")
    @pytest.mark.parametrize('data', [
        {"jobName": "UI自動(dòng)化測(cè)試任務(wù)", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.2",
         "frequency": "5min", "point": ["順德(內(nèi)網(wǎng))"], "status": "下發(fā)中"}])
    def test_add_health_success(self, data):
        """
        添加撥測(cè)任務(wù)成功
        :param data: 用例數(shù)據(jù)
        :return:
        """
        healthpage = HealthPage(self.page)
        with allure.step("1.添加健康撥測(cè)任務(wù)成功;2.點(diǎn)擊刷新;3.驗(yàn)證添加信息匹配(斷言撥測(cè)名稱(chēng)琼腔,撥測(cè)地址瑰枫,添加成功任務(wù)狀態(tài)為下發(fā)中)"):
            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("用例標(biāo)題:test_add_health_fail")
    @pytest.mark.parametrize('data', [
        {"jobName": "", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "point": ["順德(內(nèi)網(wǎng))"], "error": "監(jiān)控名稱(chēng)不能為空"},
        {"jobName": "UI自動(dòng)化測(cè)試任務(wù)", "systemId": "", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "point": ["順德(內(nèi)網(wǎng))"], "error": "歸屬系統(tǒng)不能為空"},
        {"jobName": "UI自動(dòng)化測(cè)試任務(wù)", "systemId": "T-mPTS", "env": "", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "point": ["順德(內(nèi)網(wǎng))"], "error": "環(huán)境不能為空"},
        {"jobName": "UI自動(dòng)化測(cè)試任務(wù)", "systemId": "T-mPTS", "env": "SIT", "probeType": "", "url": "127.0.0.1",
         "frequency": "5min", "point": ["順德(內(nèi)網(wǎng))"], "error": "撥測(cè)類(lèi)型不能為空"},
        {"jobName": "UI自動(dòng)化測(cè)試任務(wù)", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "",
         "frequency": "5min", "point": ["順德(內(nèi)網(wǎng))"], "error": "例如:10.1.1.1或onemonitorsit.midea.com"},
        {"jobName": "UI自動(dòng)化測(cè)試任務(wù)", "systemId": "T-mPTS", "env": "SIT", "probeType": "ICMP", "url": "127.0.0.1",
         "frequency": "5min", "error": "撥測(cè)點(diǎn)不能為空"}
    ])
    def test_add_health_fail(self, data):
        """
        驗(yàn)證添加用例健康撥測(cè)失敗場(chǎng)景
        :param data:
        :return:
        """
        healthpage = HealthPage(self.page)
        with allure.step("1.添加健康撥測(cè)任務(wù)失敗;2.提示驗(yàn)證"):
            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"):
    """用于調(diào)試頁(yè)面page時(shí)打印日志"""
    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

    # 創(chuàng)建滾動(dòng)文件處理器
    rotating_handler = logging.handlers.RotatingFileHandler(
        filename=log_path_file, maxBytes=max_log_size, backupCount=backup_count)
    rotating_handler.setFormatter(log_formatter)

    # 創(chuàng)建控制臺(tái)處理器
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(log_formatter)

    # 創(chuàng)建日志記錄器
    logger = logging.getLogger()
    logger.addHandler(rotating_handler)  # 添加滾動(dòng)文件處理器
    logger.addHandler(console_handler)  # 添加控制臺(tái)處理器
    logger.setLevel(logging.INFO)

    return logger






common/conf_yaml.py

import os
import yaml

def get_conf(*key):
    """
    獲取配置
    :param key:需要獲取的配置名稱(chēng)
    :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

四,執(zhí)行用例后查看
執(zhí)行結(jié)果:


image.png

image.png

五丹莲,查看allure測(cè)試報(bào)告:


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末光坝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子甥材,更是在濱河造成了極大的恐慌盯另,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洲赵,死亡現(xiàn)場(chǎng)離奇詭異鸳惯,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)叠萍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)芝发,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人苛谷,你說(shuō)我怎么就攤上這事辅鲸。” “怎么了腹殿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵独悴,是天一觀的道長(zhǎng)例书。 經(jīng)常有香客問(wèn)我,道長(zhǎng)绵患,這世上最難降的妖魔是什么雾叭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮落蝙,結(jié)果婚禮上织狐,老公的妹妹穿的比我還像新娘。我一直安慰自己筏勒,他們只是感情好移迫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著管行,像睡著了一般厨埋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上捐顷,一...
    開(kāi)封第一講書(shū)人閱讀 49,784評(píng)論 1 290
  • 那天荡陷,我揣著相機(jī)與錄音,去河邊找鬼迅涮。 笑死废赞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的叮姑。 我是一名探鬼主播唉地,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼传透!你這毒婦竟也來(lái)了耘沼?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤朱盐,失蹤者是張志新(化名)和其女友劉穎群嗤,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體兵琳,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骚烧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闰围。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡既峡,死狀恐怖羡榴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情运敢,我是刑警寧澤校仑,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布忠售,位于F島的核電站,受9級(jí)特大地震影響迄沫,放射性物質(zhì)發(fā)生泄漏稻扬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一羊瘩、第九天 我趴在偏房一處隱蔽的房頂上張望泰佳。 院中可真熱鬧,春花似錦尘吗、人聲如沸逝她。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)黔宛。三九已至,卻和暖如春擒贸,著一層夾襖步出監(jiān)牢的瞬間臀晃,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工介劫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留徽惋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓蜕猫,卻偏偏與公主長(zhǎng)得像寂曹,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子回右,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348