转测试开发之 “web自动化测试” 急速版

AI摘要
【知识分享】本文介绍了基于Python、pytest、Selenium和Allure的Web自动化测试框架搭建与脚本编写。内容涵盖环境依赖安装、测试用例数据驱动(通过Excel读取)、核心测试脚本(demo.py)结构、Allure报告集成方法,以及通过conftest.py管理Selenium WebDriver和自定义Keywords操作类(封装点击、输入、悬停等常用方法)的关键实现细节。

环境搭建

python
allure 用来生成测试报告 分为客户端python依赖 都需要安装
Jenkins 如果需要自动化目前我只了解到用它可以实现
selecnium 帮助我们通过选中浏览器中的元素
pytest 测试框架
执行测试命令python demo.py

这里python demo.py 这不是这一种方案,只是因为我使用了__name__ = "__main__",其他操作文件或者系统的依赖不在叙述自行学习,还有selecnium 不是唯一方案,建议去了解一下 selecnium 和 webdriver的关系

脚本编写

直接上代码,像我这样自己读一下代码行基本上手了
其中keyworld参数是通过 装饰器 @pytest.fixture 在conftest.py中定义,这也是个知识点
我的登录方案其实有点误解了,这里我直接承认,当你认真学了POM和KDT两种模式就明白。

demo.py

import pandas as pd
import pytest
import os
import subprocess
import allure

def attach_screenshot_to_allure(driver, step_name="步骤截图"):
    """
    截图并附加到 Allure 报告的辅助函数
    driver: WebDriver 实例
    step_name: 截图名称
    """
    try:
        screenshot = driver.get_screenshot_as_png()
        allure.attach(
            screenshot,
            name=step_name,
            attachment_type=allure.attachment_type.PNG
        )
    except Exception as e:
        print(f"截图失败: {e}")

PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def load_cases_from_excel(path):
    df = pd.read_excel(path)
    df = df.fillna("")
    cases = df.to_dict(orient="records")
    return cases

cases = load_cases_from_excel("files/2026-01-12.xlsx")


@pytest.mark.parametrize("case_info", cases)
def test_demo(keywords, case_info):
    # 使用 allure 步骤记录测试过程
    with allure.step(f"执行关键字: {case_info.get('关键字', '未知')}"):
        print("=== 执行到 test_demo 了 ===")
        print(case_info["关键字"])
        key = case_info.get("关键字", "").strip()  # 获取关键字并去除空格

        # 检查关键字是否为空
        if not key:
            allure.attach(
                "关键字为空",
                name="跳过原因",
                attachment_type=allure.attachment_type.TEXT
            )
            pytest.skip("关键字为空,跳过此用例")

        print(case_info)

        # 检查方法是否存在
        if not hasattr(keywords, key):
            allure.attach(
                f"Keywords 类没有 '{key}' 方法",
                name="跳过原因",
                attachment_type=allure.attachment_type.TEXT
            )
            pytest.skip(f"Keywords 类没有 '{key}' 方法")

        # 检查是否登录(需要先检查属性是否存在,避免 AttributeError)
        if not hasattr(keywords, 'login_success'):
            # 如果还没有执行过 check_login,先不判断
            pass
        elif keywords.login_success and case_info["登录操作"] != "":
            allure.attach(
                "已登录状态",
                name="跳过原因",
                attachment_type=allure.attachment_type.TEXT
            )
            allure.attach(
                str(keywords.login_success),
                name="登录状态",
                attachment_type=allure.attachment_type.TEXT
            )
            pytest.skip("已登录,跳过此用例")

        # 执行关键字操作
        with allure.step(f"调用方法: {key}"):
            key_func = getattr(keywords, key)  # 用 getattr 更安全
            key_func(**case_info)

            # 如果执行成功,记录到报告
            allure.attach(
                str(case_info),
                name="执行参数",
                attachment_type=allure.attachment_type.TEXT
            )

            # 截图并附加到报告
            attach_screenshot_to_allure(keywords.driver, f"步骤截图-{key}")


if __name__ == "__main__":
    try:
        allure_results_dir = "allure-results"
        allure_report_dir = "allure-report"
        os.makedirs(allure_results_dir, exist_ok=True)

        # 运行测试(加上日志收集参数,让 allure 能捕获日志)
        exit_code = pytest.main([
            "-vs",
            "web/demo.py",
            "--alluredir", allure_results_dir,
            "--log-cli-level=INFO",  # 收集 INFO 级别及以上的日志
            "--log-cli-format=%(asctime)s [%(levelname)s] %(message)s",  # 日志格式
            "--log-cli-date-format=%Y-%m-%d %H:%M:%S"  # 时间格式
        ])

        # 测试完成后,生成并打开 allure 报告
        try:
            # 生成 HTML 报告
            subprocess.run([
                "allure", "generate", allure_results_dir,
                "-o", allure_report_dir,
                "--clean"
            ], check=True, cwd=PROJECT_ROOT)

            # 打开报告(会自动在浏览器打开)
            # 注意:allure open 会启动一个服务器,需要手动关闭(Ctrl+C)
            print("\n正在打开 Allure 报告...")
            print("提示:按 Ctrl+C 可以关闭报告服务器")
            subprocess.run([
                "allure", "open", allure_report_dir
            ], check=True, cwd=PROJECT_ROOT)
        except KeyboardInterrupt:
            # 用户按 Ctrl+C 中断,优雅退出
            print("\n\n报告服务器已关闭,程序退出")
        except subprocess.CalledProcessError as e:
            print(f"生成 allure 报告失败: {e}")
            print("请确保已安装 allure 命令行工具")
        except FileNotFoundError:
            print("未找到 allure 命令,请先安装 allure")
            print("安装方法: brew install allure 或手动下载安装")
    except KeyboardInterrupt:
        # 用户按 Ctrl+C 中断整个程序,优雅退出
        print("\n\n程序被用户中断,退出")
        exit(0)

conftest.py

# conftest.py

import pytest
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from .extend.Keywords import Keywords

@pytest.fixture(scope="session")
def driver():
    options = Options()
    # options.add_argument("--headless")
    # options.add_argument("--disable-gpu")
    # options.add_argument("--no-sandbox")
    # options.add_argument("--disable-dev-shm-usage")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()

@pytest.fixture(scope="session")
def selenium_tools(driver):
    """Selenium 工具类,封装所有依赖"""
    class SeleniumTools:
        def __init__(self, driver):
            self.driver = driver
            self.time = time
            self.ActionChains = ActionChains
            self.WebDriverWait = WebDriverWait
            self.EC = EC
            self.By = By

    return SeleniumTools(driver)

@pytest.fixture(scope="session")
def keywords(driver, selenium_tools):
    """创建 Keywords 实例,注入 driver 和工具依赖"""
    return Keywords(driver, selenium_tools)

Keywords.py

# 常用操作

class Keywords:
    def __init__(self, driver, tools=None):
        self.driver = driver
        self.tools = tools  # 注入的工具依赖

    def open(self, **keyword):
        self.driver.get(keyword["数据内容"])

    def click(self, **keyword):
        """
        点击元素(带等待和滚动)
        keyword参数:
        - 定位方式: 定位方式(如 By.ID, By.XPATH)
        - 目标对象: 定位值
        - 等待时间: 可选,默认 10 秒
        """
        from selenium.common.exceptions import TimeoutException

        locator_type = keyword["定位方式"]
        locator_value = keyword["目标对象"]
        timeout = keyword.get("等待时间", 10)  # 可配置等待时间,默认 10 秒

        # 使用注入的工具
        wait = self.tools.WebDriverWait(self.driver, timeout)

        try:
            element = wait.until(self.tools.EC.element_to_be_clickable((locator_type, locator_value)))
        except TimeoutException:
            # 超时时截图并抛出更详细的错误
            screenshot = self.driver.get_screenshot_as_png()
            error_msg = (
                f"等待元素超时({timeout}秒)\n"
                f"定位方式: {locator_type}\n"
                f"定位值: {locator_value}\n"
                f"当前URL: {self.driver.current_url}"
            )
            print(error_msg)
            # 可以在这里附加截图到 allure(如果需要在报告中显示)
            raise TimeoutException(error_msg)

        # 滚动到元素位置(确保元素在视口中)
        self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
        self.tools.time.sleep(0.3)  # 等待滚动完成

        # 尝试普通点击,如果失败则使用 JavaScript 点击
        try:
            element.click()
        except Exception as e:
            # 如果普通点击失败,使用 JavaScript 点击
            print(f"普通点击失败,使用 JavaScript 点击: {e}")
            self.driver.execute_script("arguments[0].click();", element) 

    def on_input(self, **keyword):
        self.driver.find_element(keyword["定位方式"], keyword["目标对象"]).send_keys(keyword["数据内容"])

    def wait(self, **keyword):
        self.tools.time.sleep(keyword["数据内容"])

    def get_screenshot_png(self):
        """获取截图的 PNG 二进制数据(用于附加到报告)"""
        return self.driver.get_screenshot_as_png()

    def check_login(self, **keyword):
        """
        检查元素是否存在
        keyword参数:
        - 定位方式: 定位方式(如 By.ID, By.XPATH)
        - 目标对象: 定位值
        返回: True(找到元素)/ False(找不到元素)
        """
        from selenium.common.exceptions import NoSuchElementException

        try:
            self.driver.find_element(keyword["定位方式"], keyword["目标对象"])
            self.login_success = False  # Python 中布尔值首字母大写
            return False
        except NoSuchElementException:
            self.login_success = True  # Python 中布尔值首字母大写
            return True

    def hover_and_click(self, **keyword):
        """
        悬停到元素上,然后点击另一个元素
        keyword参数:
        - 定位方式: 定位方式(如 By.ID, By.XPATH)
        - 悬停对象: 要悬停的元素定位值
        - 目标对象: 要点击的元素定位值
        """
        # 找到要悬停的元素
        hover_element = self.driver.find_element(keyword["定位方式"], keyword["悬停对象"])

        # 使用注入的 ActionChains 实现悬停
        actions = self.tools.ActionChains(self.driver)
        actions.move_to_element(hover_element).perform()

        # 等待一下,确保悬停效果生效
        self.tools.time.sleep(0.5)

        # 点击目标元素(使用等待和滚动)
        from selenium.common.exceptions import TimeoutException

        locator_type = keyword["定位方式"]
        locator_value = keyword["目标对象"]
        timeout = keyword.get("等待时间", 10)

        wait = self.tools.WebDriverWait(self.driver, timeout)

        try:
            click_element = wait.until(self.tools.EC.element_to_be_clickable((locator_type, locator_value)))
        except TimeoutException:
            # 超时时截图并抛出更详细的错误
            screenshot = self.driver.get_screenshot_as_png()
            error_msg = (
                f"悬停后等待点击元素超时({timeout}秒)\n"
                f"定位方式: {locator_type}\n"
                f"定位值: {locator_value}\n"
                f"当前URL: {self.driver.current_url}"
            )
            print(error_msg)
            raise TimeoutException(error_msg)

        # 滚动到元素位置
        self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", click_element)
        self.tools.time.sleep(0.3)

        # 尝试普通点击,如果失败则使用 JavaScript 点击
        try:
            click_element.click()
        except Exception as e:
            # 如果普通点击失败,使用 JavaScript 点击
            print(f"普通点击失败,使用 JavaScript 点击: {e}")
            self.driver.execute_script("arguments[0].click();", click_element)

总结

祝你好运!

本作品采用《CC 协议》,转载必须注明作者和本文链接
保持好奇,求知若饥,终身编程
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!