# qingcloud-playwright **Repository Path**: tython/qingcloud-playwright ## Basic Information - **Project Name**: qingcloud-playwright - **Description**: Pitrix-UI E2E自动化测试框架 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: main - **Homepage**: https://gitee.com/tython/qingcloud-playwright - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 2 - **Created**: 2024-05-20 - **Last Updated**: 2026-02-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Pitrix-UI E2E自动化测试框架 ## 1.安装部署 ```text 测试对象: QingCloud 公有云、企业云v7 python版本: 3.9 ``` 拉取项目 ```git git clone git@git.yunify.com:yiweitang/qingcloud-playwright.git ``` 安装 poetry包管理工具 ```pip pip install poetry ``` 安装项目依赖包 ```text poetry install ``` 安装浏览器驱动 ```text playwright install ``` 代码格式化(选做) ```text black 代码格式化: black . isort import语句排序: isort . flake8 代码质量分析: flake8 . ```` 导出依赖(上传git前若安装有第三方包,则需要更新依赖) ```poetry export poetry export --without-hashes -f requirements.txt --output requirements.txt ``` ## 2.框架结构说明 - [auth](auth) 登录认证相关会话保存目录 - [apis](apis) qingcloud 各模块的api封装 - [business](business) 业务层的操作封装 - [common](common) 公共 - [components](components) 前端公共组件组封装 - [config](config) 配置文件 - [constant](constant) 一些常量 - [datas](datas) 测试数据 - [deploy](deploy) 部署相关 - [logs](logs) 执行测试相关日志 - [models](models) 页面对象封装,采用传统pom - [plugin](plugin) 第三方插件 - [reports](reports) 测试报告 - [test-results](test-results) 测试记录trace - [testcases](testcases) 测试用例目录 - [utils](utils) 自定义工具库 - [Dockerfile](Dockerfile) 运行在linux系统下的docker打包文件 - [Jenkinsfile](Jenkinsfile) 在kubesphere上运行的流水线文件 - [main.py](main.py) 测试主入口 - [pyproject.toml](pyproject.toml) poetry 配置文件和一些python依赖配置 ## 3.业务层 ### 3.1.业务层封装说明 - [models](models) 对应不同的产品页面,大致可分为资源创建页、资源列表页、资源详情页, 所有页均继承自 base_page; - [business](business) 多个连续的页面元素对象组成的业务操作,其中子目录按业务模块划分,主要分为计算、存储、网络、安全、公共服务等; - [testcases](testcases) 主要调用 business 中封装好的步骤组成场景的测试用例; ### 3.2 POM说明 一个页面算作一个pom,以VPC产品为例,一共只有3个页面 VPC主页算一个pom ![vpc_home.png](datas%2Fframe%2Fvpc_home.png) VPC创建页面算一个pom ![create_vpc.png](datas%2Fframe%2Fcreate_vpc.png) VPC详情页算一个pom ![vpc_detail.png](datas%2Fframe%2Fvpc_detail.png) ```python # vpc页面需要将创建页和详情页进行实例化 class RouterPage(BasePage): def __init__(self, page: Page): super().__init__(page) self.route = "routers" self.create_page = CreateRouterPage(page) self.detail_page = RouterDetailPage(page) ``` ```python # 业务层面 router_page 作为唯一入口 class RouterBusiness(BasePage): def __init__(self, page: Page): super().__init__(page) self.router_page = RouterPage(page) ``` 注意:页面上的弹框不算一个单独的POM,以vxnet页面为例, 因为页面路由 https://console.qingcloud.com/pek3/vxnets 不变,不算一个单独页面 ![vxnet_home.png](datas%2Fframe%2Fvxnet_home.png) ### 3.3 数据库说明 [pitrix-ui.db](databases%2Fpitrix-ui.db) 数据库记录本次测试是所有数据,在开始测试时自动创建,测试结束时保留,直到下次运行测试再次自动清理重建 测试用例表 ![testcase_table.png](datas%2Fframe%2Ftestcase_table.png) 请求记录表 ![request_table.png](datas%2Fframe%2Frequest_table.png) 响应记录表 ![response_table.png](datas%2Fframe%2Fresponse_table.png) 每个测试环境对应一个配置文件,保存在 [config](config) 目录下,命名规则以【环境名_config.yaml】来命名 ![test_config.png](datas%2Fframe%2Ftest_config.png) 取对应环境配置文件的方式如下: ```python @staticmethod def load_yaml_config_to_cache() -> None: """ 将配置文件写入缓存 :return: """ env = os.environ['TEST_ENV'] filename = f"{env}_config.yaml" config_file = CONFIG_DIR / filename if not config_file.exists(): log.warning(f"警告: 文件 {config_file} 不存在,请检查配置文件") sys.exit(1) log.info(f"正在加载{env} 环境的配置文件: {config_file}") config = YamlHandler(config_file).read_yaml() cache_manager.set('env', env) for key, value in config.items(): cache_manager.set(key, value) ``` 配置文件统一存到 [auth](auth)/[cache](auth%2Fcache) 下的数据库中,保存的内容如下: ![test_config_from_db.jpg](datas%2Fframe%2Ftest_config_from_db.jpg) 测试时需要取配置通过 `cache_manager.get(key)` 获取 ```python from utils.cache_util import cache_manager regions = cache_manager.get("regions") ``` ### 3.4.异步任务及租赁信息处理 #### 3.4.1 异步任务 1.通过QingCloud API 发送请求轮询异步任务的状态,直到任务完成(成功或失败); #### 3.4.2 租赁信息 1.界面UI创建资源完成时,通过截取响应获取到资源ID,通过QingCloud API 发送请求轮询租赁信息状态,达到预期状态后退出轮询; 如下所示,MixinUtil类通过继承方式注入 BasePage 类,并注入到业务层中 ```python class MixinUtil: @staticmethod def ensure_directory_exists(directory: Path): """确保目录存在,如果不存在则创建""" if not directory.exists(): directory.mkdir(parents=True, exist_ok=True) logger.info(f"创建目录: {directory}") def wait_jobs(self, response: Dict[str, Any], timeout: int = 600, interval: int = 5, check_status: bool = False) -> [str, None]: return wait_jobs(response, timeout, interval, check_status) @allure.step("等待资源租赁信息") def wait_lease_status( self, resources: List[str], status: Union[List[str], Tuple[str], Set[str]], time_out: int = 600, ) -> None: """ 等待资源租赁状态就绪 @param resources: 等待的资源 @param status: 状态 @param time_out: 设置超时时间 """ user_id = cache_manager.get('user_id') if not isinstance(resources, list): resources = [resources] if cache_manager.get('wait_leased'): logger.info(f"正在等待资源:{resources} 的租赁信息中...") no_ready_res = deepcopy(resources) cnt = 0 sleep_interval = 10 while cnt < time_out / sleep_interval: for resource_id in no_ready_res: rep = billing_client.get_lease_info(resource_id, user=user_id) ath.json(body=rep, name="查询资源租赁信息") logger.info(rep) lease_info = rep['lease_info'] if lease_info['status'] in to_list(status): no_ready_res.remove(resource_id) if len(no_ready_res) == 0: break cnt += 1 time.sleep(sleep_interval) case_assert.assert_eq( 0, len(no_ready_res), "等待资源 [%s] 的租赁信息为 [%s] 状态时在billing server超时" % (no_ready_res, status), ) else: logger.info(f"当前环境:{cache_manager.get('env')} 跳过租赁信息检查") ``` ## 4.测试说明 ### 4.1 测试环境 测试时指定 --env 来指定测试环境 ```shell # 通过pytest执行 pytest testcases/test_qingcloud -m home --env=qingcloud --headless=false # 通过程序入口文件执行 python main.py --env=qingcloud --mark=home --headless ``` ### 4.2 测试参数说明 ![startup_parameters.jpg](datas%2Fframe%2Fstartup_parameters.jpg) ### 4.3 单进程执行用例 ![run_testcase.png](datas%2Fframe%2Frun_testcase.png) ### 4.4 多进程执行用例 ```shell # 默认为 auto 模式,不指定mp_count则自动根据cpu核心数来确定进程数 python main.py --env=qingcloud --mp --mp_count=4 ``` ![mp_execute1.jpg](datas%2Fframe%2Fmp_execute1.jpg) ![mp_execute2.jpg](datas%2Fframe%2Fmp_execute2.jpg) ### 4.5 测试日志 ![test_step.png](datas%2Fframe%2Ftest_step.png) ![test_log.png](datas%2Fframe%2Ftest_log.png) ### 4.6 断言说明 每做一步操作都需要对操作的内容做断言 1.例如勾选后检查元素是否已选中 2.导航到目标页面后检查url、title等是否符合预期,页面显示出来各模块元素是否可见 3.创建、修改、删除资源后页面检查资源是否存在、属性是否变更等 4.打开某个弹框后检查弹框内容是否符合预期,标志性文案是否可见等 5.元素的存在与可见性 6.交互功能响应是否正常 7.数据输入输出限制和规则验证 8.按钮的启用、禁用状态,成功、错误标志验证 9.视音频播放是否正常 10.上传、下载是否正常 11.鼠标、键盘事件是否正常 12.国际化与本地化是否正常 ### 4.7 测试报告配置 [notification.yaml](config%2Fnotification.yaml) 下配置 webhook 和 allure server,用于接收测试结果 配置有效的webhook 可接收到下面的测试结果的消息通知 ![test_message.png](datas%2Fframe%2Ftest_message.png) 配置有效的allure server端口和地址 可自动上传测试报告到allure server ![test_report.png](datas%2Fframe%2Ftest_report.png) ## 5.其他 ### 5.1 playwright常用方法 playwright 常用方法 ``` 1.page.wait_for_selector() 方法,如果没有传 state 参数,默认情况下是等待元素可见 visible 等待元素出现在DOM中:page.wait_for_selector("定位方法", state='attached') 等待从DOM中移除:page.wait_for_selector("定位方法", state='detached') 等待元素可见:page.wait_for_selector("定位方法", state="visible") 等待元素不可见:page.wait_for_selector("定位方法", state='hidden') 2.wait_for() 方法 另外一个先定位元素,再使用wait_for() 方法也可以等待元素到达指定的状态。 page.locator('.toast-message').wait_for(state="detached") 3.自定义异常及超时时间 expect(page.get_by_text("Name"), "should be logged in").to_be_visible(timeout=3000) 4.其他(略) ``` ### 5.2 部署allure-server (可选) ```text docker pull registry.cn-chengdu.aliyuncs.com/yiweitang/allure-server:latest docker run -d -p 8080:8080 --restart=always registry.cn-chengdu.aliyuncs.com/yiweitang/allure-server:latest --name allure-server ``` ### 5.3 部署qingcloud-playwright (可选) ```text 测试用例在Linux上运行时, 打包前需要将headless需要改为True docker build -t qingcloud/qingcloud-playwright:v0.1 . docker run -it --rm --ipc=host --security-opt seccomp=seccomp_profile.json qingcloud/qingcloud-playwright:v0.1 /bin/bash ``` ### 5.4 录制与浏览器接管(开发阶段) 录制 ```text playwright codegen https://account.qingcloud.com/login playwright codegen http://console.testbmcloud.com/login ``` trace追踪 ```text playwright show-trace trace.zip ``` 设置环境变量 ```text mac: 添加chrome路径到系统环境变量中的启动方式 export PATH="/Applications/Google\ Chrome.app/Contents/MacOS:$PATH" alias chrome="Google\ Chrome" chrome --remote-debugging-port=12345 --incognito -–start-maximized --new-window https://console.qingcloud.com/ --user-data-dir="/Users/tyw/Desktop/tmp" 未设置环境变量,通过完成CLI启动方式 /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=12345 --incognito -–start-maximized --new-window https://console.qingcloud.com/ --user-data-dir="/Users/tyw/Desktop/tmp" win: 添加chrome路径到path环境变量中的启动方式 chrome --remote-debugging-port=12345 -–start-maximized --new-window https://account.qingcloud.com/login 未设置环境变量,通过完成CLI启动方式 chrome.exe --remote-debugging-port=12345 --incognito -–start-maximized --new-window https://account.qingcloud.com/login --user-data-dir="C:\tmp" ``` ### 5.5 常用的定位方法 #### 5.5.1 Xpath定位 ```text 1.绝对路径:从根节点/html,逐级往下(不跳跃),例如:/html/body/div/div/form/input[1] 2.相对路径: //input 表示所有input标签,例如://*[@id="kw"] 3.属性法: [@属性='属性值'],例如://*[@id="kw"];//input[@name='password'] 4.函数法 4.1.starts-with(@属性名,'属性开头的值'):定位属性以xxx开头的元素,处理属性值变化的元素,例如://*[starts-with(@id,'username')] 4.2.contains(@属性名,'属性包含的值'):匹配属性包含的值,例如://*[contains(@id,'username')] 4.3.text()='文本的值':# 定位文本值等于XXX的元素 ,可以替代link_text方法,一般适合 p标签,a标签。例如://a[text()='百度搜索'] 4.4.contains(text(),'文本包含的值'):匹配文本包含的值,可以替代partial_link_text方法,例如://a[contains(text(),'搜索')] 5.轴对称法 5.1.following-sibling: //div[@class='side-entry aging-entry']/following-sibling::div 定位同级元素的下一个元素 5.2.preceding-sibling: //div[contains(@class,'qrcode-nologin')]/preceding-sibling::div 定位同级元素的上一个元素 5.3 parent: //label[text()='网络']/parent::* 根据子元素定位父元素 ``` #### 5.5.2 CSS定位 ```text 1.基本定位表达式: 标签 标签名 类 .class属性值 id #id属性值 属性 [属性名=’属性值‘] 2.关系定位表达式: 并集 元素,元素 临近兄弟 元素+元素 兄弟 元素1~元素2 父子 元素>元素 后代 元素 元素 ```