1. 项目概述:为什么我们需要自动化测试框架?

做开发或者测试的朋友,尤其是刚入行的新人,可能都经历过这样的场景:项目上线前,你手动点了几百个页面,眼睛都花了,结果还是漏掉了一个关键流程的bug,导致线上事故。或者,每次代码有改动,你都得把核心功能再手动跑一遍,耗时耗力,还容易出错。这种重复、机械、易遗漏的工作,正是自动化测试要解决的核心痛点。

Python,凭借其语法简洁、生态丰富、社区活跃的特点,成为了自动化测试领域的首选语言之一。但“用Python写自动化测试”和“用好Python自动化测试”是两码事。前者可能只是写一些零散的脚本,后者则需要一个清晰、可维护、可扩展的体系——这就是测试框架的价值。一个好的测试框架,能帮你组织测试用例、管理测试数据、生成测试报告、集成持续集成(CI)流程,让你从“脚本小子”升级为“测试工程师”。

今天,我们不谈那些庞大复杂的商业套件,就聚焦在四个基于Python、能让你从零开始上手,并足以支撑起一个严肃项目测试需求的优秀框架。无论你是想测试Web应用、API接口,还是桌面程序,这里都有对应的解决方案。我会结合我这些年踩过的坑和积累的经验,带你从“是什么”、“怎么选”到“怎么用”,把这四个框架讲透。

2. 框架全景图:四大金刚的定位与选型

在深入细节之前,我们必须先建立一个宏观的认知。这四个框架并非互相替代,而是各有侧重,共同构成了Python自动化测试的武器库。选对工具,事半功倍。

2.1 框架核心定位解析

为了让你一目了然,我把这四个框架的核心特性和适用场景做成了下面的表格:

框架名称 核心测试类型 主要应用场景 学习曲线 生态与社区
unittest 单元测试、集成测试 Python标准库自带,所有Python项目的测试基础,尤其适合测试函数、类等代码单元。 平缓 极其成熟,是Python生态的基石。
pytest 全类型测试(单元、集成、功能) 当前Python社区事实上的单元测试标准,功能强大、插件丰富,适合任何规模和类型的测试。 中等 异常活跃,插件生态庞大。
Selenium Web UI 自动化测试 模拟用户在浏览器中的操作(点击、输入、跳转等),用于测试Web应用的前端功能和交互。 较陡 历史悠久,跨语言支持,社区庞大。
Playwright Web UI 自动化测试、API测试 新一代浏览器自动化工具,支持多浏览器(Chromium, Firefox, WebKit),功能强大,速度更快。 中等偏上 由微软维护,发展迅猛,现代Web测试首选。

选型心法:

  • 如果你是初学者,或者项目要求快速验证一个Python模块的功能 :从 unittest 开始。它是Python的一部分,无需额外安装,语法相对简单,能帮你建立测试的基本概念(如断言、测试用例、测试套件)。
  • 如果你的项目已经有一定规模,或者你希望拥有最灵活、最强大的测试工具 :直接上 pytest 。它几乎可以完成 unittest 能做的所有事情,并且更简洁、更强大。很多新项目甚至直接用它替代 unittest
  • 如果你的测试对象是Web页面,需要验证页面元素、交互流程 :在 Selenium Playwright 之间选择。
    • Selenium 更传统,资料极多,兼容性广(包括一些老版本浏览器),如果你需要支持IE等老旧浏览器,它可能是唯一选择。
    • Playwright 更现代,性能更好,内置了自动等待、网络拦截等高级功能,对现代单页应用(SPA)支持更佳。对于新项目,我强烈推荐从Playwright开始。

注意:在实际项目中,它们常常组合使用。例如,用 pytest 作为测试运行器和组织框架,用 unittest pytest 写单元测试,再用 Playwright 写端到端(E2E)的UI测试。

2.2 环境准备:万变不离其宗的起点

无论你选择哪个框架,一个干净、独立的Python环境是第一步。这里我强烈推荐使用 venv (Python 3.3+ 内置)或 conda 来创建虚拟环境。这能避免项目间的包版本冲突。

venv 为例,在项目根目录下执行:

# Windows
python -m venv venv
# 激活环境
venv\Scripts\activate

# macOS/Linux
python3 -m venv venv
# 激活环境
source venv/bin/activate

激活后,你的命令行提示符前会出现 (venv) 字样,表示你正在虚拟环境中工作。

接下来,就是通过 pip 安装所需的框架了。我们将逐一进行。

3. 基石之选:unittest —— 内置的严谨派

unittest 是Python标准库中的测试框架,深受Java的JUnit影响。它提供了测试用例(TestCase)、测试套件(TestSuite)、测试夹具(Fixture)和测试运行器(TestRunner)等完整概念。

3.1 快速上手与核心概念

假设我们有一个简单的计算器模块 calculator.py

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

为它编写 unittest 测试用例 test_calculator.py

import unittest
from calculator import add, subtract

class TestCalculator(unittest.TestCase):
    """测试计算器功能的测试类,必须继承 unittest.TestCase"""

    # 测试方法必须以 test_ 开头
    def test_add_integers(self):
        """测试整数加法"""
        self.assertEqual(add(1, 2), 3)  # 断言:期望 add(1,2) 的结果等于 3
        self.assertEqual(add(-1, 1), 0)

    def test_add_floats(self):
        """测试浮点数加法,注意浮点数精度"""
        self.assertAlmostEqual(add(0.1, 0.2), 0.3, places=7)

    def test_subtract(self):
        """测试减法"""
        self.assertEqual(subtract(5, 3), 2)
        self.assertEqual(subtract(0, 5), -5)

    # setUp 和 tearDown 是测试夹具
    def setUp(self):
        """每个测试方法执行前都会运行,用于准备测试环境"""
        print(f"\n开始执行测试: {self._testMethodName}")

    def tearDown(self):
        """每个测试方法执行后都会运行,用于清理环境"""
        print(f"测试执行完毕: {self._testMethodName}")

if __name__ == '__main__':
    unittest.main()  # 运行所有测试

在命令行执行 python test_calculator.py ,你会看到详细的测试通过或失败信息。

核心要点解析:

  1. 继承 unittest.TestCase :这是必须的,它赋予了类运行测试的能力。
  2. 方法命名 :所有测试方法必须以 test_ 开头,运行器会自动发现它们。
  3. 断言方法 self.assertEqual(a, b) 是最常用的,还有 self.assertTrue(x) , self.assertIn(a, b) , self.assertRaises(Error) 等,用于验证测试结果。
  4. 测试夹具 (setUp/tearDown)
    • setUp : 在每个测试方法 执行,常用于初始化资源(如数据库连接、打开浏览器)。
    • tearDown : 在每个测试方法 执行,常用于清理资源(如关闭连接、退出浏览器)。
    • 还有 setUpClass / tearDownClass (在整个测试类前后执行一次)和 setUpModule / tearDownModule (在整个模块前后执行一次)。

3.2 实战技巧与常见陷阱

技巧1:组织大型测试项目 对于大型项目,测试文件会很多。建议建立 tests 目录,并使用 TestLoader TestSuite 来组织。

project/
├── src/
│   └── your_module.py
└── tests/
    ├── __init__.py
    ├── test_module_a.py
    └── test_module_b.py

你可以使用 python -m unittest discover -s tests -p "test_*.py" 命令自动发现并运行 tests 目录下所有以 test_ 开头的测试文件。

陷阱1:测试隔离失败 unittest 默认会为每个测试方法创建一个新的测试类实例。但如果你在 setUpClass 中初始化了类属性(如一个全局的数据库连接),并在测试方法中修改了它,那么这个状态会影响到其他测试方法。 务必确保每个测试都是独立的 tearDown 阶段要彻底清理。

陷阱2:过于复杂的断言 有时一个测试方法里塞满了十几个 assertEqual ,一旦中间某个失败,后面的断言就不会执行,你无法看到全貌。这时可以考虑:

  • 拆分成多个更细粒度的测试方法。
  • 使用 subTest 上下文管理器(Python 3.4+),它允许在一个测试方法内运行多个子测试,即使一个失败,其他也会继续执行。
    def test_multiples_cases(self):
        test_cases = [(1,2,3), (0,0,0), (-1,1,0)]
        for a, b, expected in test_cases:
            with self.subTest(a=a, b=b, expected=expected):
                self.assertEqual(add(a, b), expected)
    

unittest 是坚实的基础,但当你需要更灵活的夹具、更简洁的语法、更强大的插件时,就该 pytest 登场了。

4. 社区王者:pytest —— 灵活强大的瑞士军刀

pytest 不是一个标准库,但它凭借其简洁的语法和强大的功能,几乎成为了Python单元测试的新标准。它的哲学是“约定优于配置”和“尽可能少的样板代码”。

4.1 安装与极简入门

首先安装: pip install pytest

沿用上面的 calculator.py ,用 pytest 重写测试,文件命名为 test_calc_pytest.py pytest 默认也会查找 test_*.py 文件):

# test_calc_pytest.py
from calculator import add, subtract

# 测试函数名以 test_ 开头即可,不需要继承任何类
def test_add_integers():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0

def test_add_floats():
    # pytest 使用标准的 assert 语句,更符合Python习惯
    assert abs(add(0.1, 0.2) - 0.3) < 1e-7  # 自己处理浮点精度

def test_subtract():
    assert subtract(5, 3) == 2

在命令行直接运行 pytest test_calc_pytest.py 。看,不需要 unittest.main() ,不需要特殊的断言方法,直接用 assert ,失败了 pytest 会给出非常清晰的错误信息,包括表达式的值。

4.2 核心特性深度解析

1. 夹具(Fixtures):超越 setUp/tearDown pytest 的夹具是其灵魂。它通过 @pytest.fixture 装饰器定义,比 unittest setUp 更灵活、更可复用。

import pytest

@pytest.fixture
def db_connection():
    """模拟一个数据库连接夹具"""
    print("\n=== 建立数据库连接 ===")
    connection = {"connected": True}  # 模拟连接对象
    yield connection  # yield 之前是 setup,之后是 teardown
    print("=== 关闭数据库连接 ===")
    connection["connected"] = False

def test_query_user(db_connection):  # 将夹具作为参数传入测试函数
    """测试函数依赖 db_connection 夹具"""
    assert db_connection["connected"] is True
    # 执行查询逻辑...
    print("执行用户查询测试")

def test_update_product(db_connection):  # 多个测试可以复用同一个夹具
    assert db_connection["connected"] is True
    # 执行更新逻辑...
    print("执行产品更新测试")

运行测试,你会看到每个测试函数执行时,连接建立和关闭的日志。 yield 模式让资源管理变得异常清晰。夹具还可以设置作用域( scope="session" / "module" / "class" / "function" ),实现不同级别的共享。

2. 参数化测试:一行代码覆盖多组数据 这是 pytest 的一大杀器,能极大减少重复代码。

import pytest
from calculator import add

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, -50, 50),
])
def test_add_parametrized(a, b, expected):
    """用一组参数测试 add 函数"""
    assert add(a, b) == expected

运行后, pytest 会将其展开为4个独立的测试用例,并分别报告结果。

3. 丰富的插件生态

  • pytest-html : 生成漂亮的HTML测试报告。 pip install pytest-html ,然后运行 pytest --html=report.html
  • pytest-cov : 生成测试覆盖率报告。 pip install pytest-cov ,运行 pytest --cov=your_module
  • pytest-xdist : 并行运行测试,加速测试套件。 pip install pytest-xdist ,运行 pytest -n auto (根据CPU核心数自动分配)。
  • pytest-mock : 集成 unittest.mock ,方便进行模拟(Mock)和打桩(Stub)。

4.3 高级用法与排错指南

标记(Markers)与选择性运行 你可以给测试打上标签,然后只运行特定的测试。

import pytest

@pytest.mark.slow  # 自定义一个‘慢速测试’标记
def test_complex_calculation():
    import time
    time.sleep(2)  # 模拟耗时操作
    assert 1 == 1

@pytest.mark.quick  # ‘快速测试’标记
def test_simple_check():
    assert True

# 在命令行中运行:pytest -m quick   # 只运行标记为 quick 的测试
# 运行:pytest -m "not slow"      # 运行所有非 slow 标记的测试

需要在项目根目录创建一个 pytest.ini 文件来注册自定义标记,避免警告:

[pytest]
markers =
    slow: marks tests as slow (deselect with '-m \"not slow\"')
    quick: marks tests as quick to run

常见问题排查

  • 问题:夹具找不到(FixtureNotFoundError)
    • 原因 :测试函数请求的夹具没有定义,或者定义它的文件没有被 pytest 正确发现。
    • 解决 :确保夹具定义在测试文件内,或者在一个名为 conftest.py 的文件中。 pytest 会自动发现项目目录树中所有 conftest.py 文件并加载其中的夹具,供所有测试文件使用。这是共享夹具的最佳实践。
  • 问题:测试通过了但代码改动后没反应
    • 原因 :可能是模块缓存。或者测试文件/被测代码文件不在 pytest 的搜索路径中。
    • 解决 :使用 pytest --tb=short -v 查看更详细的输出。确保运行 pytest 时所在的目录正确,或者使用 python -m pytest 命令来运行。对于缓存问题,可以尝试删除 __pycache__ 目录和 .pytest_cache 目录。
  • 问题:并行测试(pytest-xdist)时资源冲突
    • 原因 :多个测试进程同时访问同一个文件、端口或数据库。
    • 解决 :使用夹具为每个测试进程创建独立的资源,例如为每个进程生成唯一的临时目录或数据库名。可以使用 pytest worker_id 夹具来区分不同的工作进程。

pytest 的强大远不止于此,但它足以让你构建一个非常健壮的后端测试体系。接下来,我们把目光投向浏览器,看看如何自动化Web操作。

5. 浏览器操控者:Selenium —— 经典而稳健

Selenium 的核心是 WebDriver,它是一个跨语言的协议,允许你用代码像真实用户一样操作浏览器。Python 通过 selenium 包提供了绑定。

5.1 环境搭建与第一个脚本

  1. 安装Selenium库 pip install selenium
  2. 下载浏览器驱动 :这是关键一步。你需要下载与你电脑上浏览器版本匹配的驱动。
    • Chrome : 下载 ChromeDriver
    • Firefox : 下载 geckodriver
    • Edge : 下载 Microsoft Edge WebDriver 将下载的驱动文件(如 chromedriver.exe )放在一个目录下,并将该目录添加到系统的 PATH 环境变量中,或者直接在代码里指定驱动路径。

一个最简单的示例,打开百度并搜索:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time

# 1. 创建浏览器驱动实例,这里以Chrome为例
# 如果驱动已在PATH中,可以直接 webdriver.Chrome()
driver = webdriver.Chrome()  # 也可以指定路径:webdriver.Chrome(executable_path=r'你的路径\chromedriver')

try:
    # 2. 打开网页
    driver.get("https://www.baidu.com")
    time.sleep(2)  # 等待页面加载,实际项目中应用显式等待替代

    # 3. 定位元素并操作
    # 找到搜索框,输入文本
    search_box = driver.find_element(By.ID, "kw")  # 百度搜索框的id是'kw'
    search_box.send_keys("Selenium自动化测试")
    search_box.send_keys(Keys.RETURN)  # 模拟回车键

    time.sleep(3)  # 等待搜索结果加载

    # 4. 断言验证
    # 检查页面标题或某个结果元素
    assert "Selenium自动化测试" in driver.title
    print("测试通过!页面标题包含搜索关键词。")

    # 例如,获取第一个结果的标题
    # first_result = driver.find_element(By.CSS_SELECTOR, '#content_left h3 a')
    # print(f"第一个结果是: {first_result.text}")

finally:
    # 5. 关闭浏览器
    driver.quit()  # 关闭浏览器并释放驱动资源

5.2 元素定位与等待策略:稳定性的关键

元素定位八大法 find_element(By.XXX, “value”) 是核心。 By.XXX 包括:

  • By.ID : 最优先选择,通常唯一且稳定。
  • By.NAME : 次选。
  • By.CLASS_NAME : 注意类名可能有空格(多个类),需完整匹配或使用CSS选择器部分匹配。
  • By.TAG_NAME : 如 "input" , "a"
  • By.LINK_TEXT : 精确匹配链接文本。
  • By.PARTIAL_LINK_TEXT : 部分匹配链接文本。
  • By.CSS_SELECTOR : 非常强大且常用 ,语法与前端CSS一致。如 #kw (ID), .s_ipt (class), input[name='wd']
  • By.XPATH : 同样强大 ,可以在DOM树中导航。如 //input[@id='kw'] 。功能最强但写起来复杂,性能可能稍差。

经验之谈:优先使用 ID > NAME > CSS_SELECTOR CSS_SELECTOR 通常比 XPATH 性能更好,也更易读。尽量避免使用包含索引的绝对路径(如 /html/body/div[3]/div[2]/form/span[1]/input ),因为页面结构一变就失效。

等待:告别 time.sleep 的噩梦 time.sleep(固定时间) 是极不推荐的,它要么浪费等待时间,要么在网速慢时导致元素找不到而失败。Selenium 提供了两种智能等待:

  1. 隐式等待 (Implicit Wait) :为整个 driver 会话设置一个全局的最大等待时间,在查找元素时,如果元素没有立即出现,会轮询查找直到超时。

    driver.implicitly_wait(10)  # 单位:秒
    element = driver.find_element(By.ID, “someId”) # 会等待最多10秒
    

    缺点 :它只对 find_element 系列方法有效,并且无法等待更复杂的条件(如元素可点击、元素包含特定文本)。

  2. 显式等待 (Explicit Wait) 这是最佳实践 。针对某个特定条件进行等待,更精确、更灵活。

    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    
    # 等待最多10秒,直到ID为‘kw’的元素出现
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "kw"))
    )
    
    # 等待元素可点击
    button = WebDriverWait(driver, 10).until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, ".submit-btn"))
    )
    button.click()
    
    # 等待页面标题包含特定文字
    WebDriverWait(driver, 10).until(
        EC.title_contains("搜索结果")
    )
    

    expected_conditions 模块提供了大量预定义条件,如 visibility_of_element_located (元素可见)、 text_to_be_present_in_element (元素包含文本)等。

5.3 与pytest结合的最佳实践

单纯的Selenium脚本不易维护和扩展。将其与 pytest 结合是标准做法。

1. 使用夹具管理浏览器生命周期 conftest.py 中创建浏览器夹具:

# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture(scope="function")  # 每个测试函数一个浏览器实例
def browser():
    """提供一个配置好的Chrome浏览器实例"""
    options = Options()
    # 常用配置
    options.add_argument('--headless')  # 无头模式,不显示浏览器窗口,适合CI环境
    options.add_argument('--disable-gpu')
    options.add_argument('--no-sandbox')
    options.add_argument('--window-size=1920,1080')

    driver = webdriver.Chrome(options=options)
    driver.implicitly_wait(5)  # 设置全局隐式等待
    yield driver
    # 测试结束后,无论成功失败,都关闭浏览器
    driver.quit()

2. 编写基于pytest的页面测试

# test_baidu_search.py
def test_baidu_search_title(browser):
    """测试百度搜索后标题变化"""
    browser.get("https://www.baidu.com")
    assert "百度" in browser.title

def test_baidu_search_functionality(browser):
    """测试百度搜索功能"""
    browser.get("https://www.baidu.com")
    search_input = browser.find_element(By.ID, "kw")
    search_input.send_keys("pytest")
    search_input.submit()  # 提交表单

    # 使用显式等待等待结果出现
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    WebDriverWait(browser, 10).until(
        EC.title_contains("pytest")
    )
    assert "pytest" in browser.title

3. 使用Page Object Model (POM) 设计模式 这是UI自动化测试的 黄金法则 。将页面封装成类,页面的元素定位和基本操作作为类的方法。测试脚本只调用这些方法,不与具体的定位符耦合。

# pages/search_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class BaiduSearchPage:
    def __init__(self, driver):
        self.driver = driver
        self.search_input = (By.ID, "kw")
        self.search_button = (By.ID, "su")

    def load(self):
        self.driver.get("https://www.baidu.com")
        WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located(self.search_input)
        )

    def search(self, keyword):
        """输入关键词并搜索"""
        input_elem = self.driver.find_element(*self.search_input)
        input_elem.clear()
        input_elem.send_keys(keyword)
        self.driver.find_element(*self.search_button).click()
        # 等待结果页加载
        WebDriverWait(self.driver, 10).until(
            EC.title_contains(keyword)
        )

然后在测试中调用:

# test_with_pom.py
def test_search_with_pom(browser):
    page = BaiduSearchPage(browser)
    page.load()
    page.search("Playwright")
    assert "Playwright" in browser.title

POM极大地提高了代码的可读性、可维护性和复用性。当页面元素发生变化时,你只需要修改对应的Page类,而不需要修改所有测试脚本。

Selenium很强大,但它在处理现代Web应用(如大量AJAX、Shadow DOM)时,有时会显得力不从心,需要编写复杂的等待和JS注入。这时,它的挑战者Playwright带来了新的解决方案。

6. 现代新贵:Playwright —— 更快、更强、更智能

Playwright由微软开发,支持Chromium、Firefox和WebKit(Safari引擎)三大浏览器内核。它从设计之初就考虑了现代Web的复杂性。

6.1 安装与初体验:感受其便捷

安装Playwright Python包并安装浏览器: pip install playwright 然后 playwright install 这条命令会下载Chromium、Firefox和WebKit的可用版本。

一个等效的百度搜索示例:

from playwright.sync_api import sync_playwright  # 同步API

def run(playwright):
    # 启动Chromium浏览器,headless=False表示显示窗口
    browser = playwright.chromium.launch(headless=False)
    # 创建上下文和页面
    context = browser.new_context()
    page = context.new_page()

    # 导航
    page.goto("https://www.baidu.com")

    # 定位和操作:Playwright支持多种选择器,这里用CSS
    page.fill('#kw', 'Playwright自动化测试')  # fill 方法会先清空再输入
    page.click('#su')  # 点击搜索按钮

    # 等待导航完成
    page.wait_for_load_state('networkidle')  # 等待网络基本空闲
    # 或者等待特定元素出现
    # page.wait_for_selector('text=Playwright')

    # 断言
    assert 'Playwright' in page.title()
    print(f"页面标题是: {page.title()}")

    # 截图(非常方便)
    page.screenshot(path='search_result.png')

    # 关闭
    context.close()
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

代码看起来更简洁了。 page.fill() page.click() 都是非常直观的高阶API。

6.2 颠覆性特性详解

1. 自动等待(Auto-waiting) 这是Playwright相比Selenium最大的优势之一。 绝大多数操作(如 click , fill , check )本身就已经内置了智能等待 。它会等待元素满足可操作条件(如可见、可点击、未禁用)后才执行操作,你基本不需要写显式等待。上面的 page.click(‘#su’) 就会自动等待该按钮可点击。

2. 强大的选择器引擎 Playwright的选择器语法非常丰富且强大:

  • 文本选择器 page.click(‘text=登录’) 点击包含“登录”文本的元素。
  • CSS选择器 page.fill(‘#username’, ‘name’)
  • XPath page.click(‘//button’)
  • React/Vue组件选择器 (需额外配置):可以直接定位到前端框架的组件,这对于测试组件库非常有用。

3. 网络拦截与模拟(Network Interception) 你可以轻松地拦截和修改网络请求,这对于测试边缘情况、模拟慢速网络或API失败场景至关重要。

# 拦截所有请求,并阻止对某些图片的请求以加速测试
page.route("**/*.{png,jpg,jpeg}", lambda route: route.abort())

# 拦截特定API请求,并返回模拟数据
def handle_route(route):
    if "/api/user" in route.request.url:
        route.fulfill(
            status=200,
            content_type="application/json",
            body='{"name": "Mock User", "id": 123}'
        )
    else:
        route.continue_()  # 其他请求继续

page.route("**/api/*", handle_route)

4. 设备模拟与移动端测试 一行代码即可模拟特定设备(如iPhone 13)的浏览器环境,包括视口大小、User-Agent、触摸事件等。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # 模拟iPhone 13
    iphone_13 = p.devices['iPhone 13']
    browser = p.chromium.launch(headless=False)
    # 创建上下文时传入设备参数
    context = browser.new_context(**iphone_13)
    page = context.new_page()
    page.goto('https://m.example.com')
    # ... 进行移动端测试

5. 异步API支持 对于高性能或需要并发执行多个浏览器操作的场景,Playwright提供了完整的异步API( async/await )。

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto('http://example.com')
        print(await page.title())
        await browser.close()

asyncio.run(main())

6.3 与pytest集成及高级场景

Playwright官方提供了 pytest-playwright 插件,让集成变得异常简单。

  1. 安装插件 pip install pytest-playwright

  2. 使用内置夹具 :插件提供了 page , context , browser 等夹具,开箱即用。

    # test_with_playwright_pytest.py
    def test_playwright_search(page):  # 直接使用 page 夹具
        page.goto("https://www.baidu.com")
        page.fill('#kw', 'Playwright pytest')
        page.click('#su')
        # 等待选择器出现,并获取其文本
        first_result = page.text_content('#content_left h3 a >> nth=0')
        assert 'Playwright' in first_result
    

    运行测试时,无需自己管理浏览器的启动和关闭,插件会自动处理。

  3. 录制与代码生成 :Playwright有一个强大的录制工具 playwright codegen

    • 在命令行运行: playwright codegen https://www.baidu.com
    • 这会打开一个浏览器和一个录制窗口。你在浏览器中的所有操作都会被实时转换成Python(或其它语言)代码。这是快速生成测试脚本原型的利器。
  4. 处理复杂场景

    • 文件上传 page.set_input_files(‘input[type=”file”]’, ‘path/to/file.pdf’) ,极其简单。
    • 下载文件 :监听 download 事件。
    • iframe frame = page.frame(name=’frame-name’) 然后 frame.click(‘button’)
    • 弹窗/对话框 page.on(‘dialog’, lambda dialog: dialog.accept()) 自动处理弹窗。

Selenium vs Playwright 最终抉择建议:

  • 选择 Selenium 如果 :你的项目需要支持非常老旧的浏览器(如IE),或者团队对Selenium有深厚积累,或者依赖某些仅支持Selenium的第三方云测试平台。
  • 选择 Playwright 如果 :你主要测试现代浏览器(Chrome, Firefox, Safari),追求更快的执行速度、更稳定的测试、更简洁的API,并且需要处理复杂的现代Web交互(如SPA、网络拦截)。对于 新项目,我几乎毫无保留地推荐Playwright

7. 融会贯通:构建你的自动化测试体系

掌握了这四个框架,你已经有能力搭建一个完整的自动化测试金字塔了。

7.1 测试金字塔实践

理想的自动化测试结构应该是金字塔形:

  • 底层(大量) 单元测试 。使用 pytest (或 unittest )测试单个函数、类。运行速度极快,是信心的基石。
  • 中层(适量) 集成测试/API测试 。使用 pytest 测试模块间的接口、数据库操作、API调用。可以结合 requests 库进行HTTP API测试。
  • 顶层(少量) 端到端(E2E)UI测试 。使用 Playwright (或 Selenium )模拟用户完整操作流程。运行慢、脆弱,但能验证核心用户旅程。

一个项目目录结构可能如下:

my_project/
├── src/                    # 源代码
├── tests/
│   ├── unit/              # 单元测试 (pytest)
│   │   ├── test_models.py
│   │   └── test_services.py
│   ├── integration/       # 集成测试 (pytest + requests)
│   │   └── test_api.py
│   ├── e2e/              # UI端到端测试 (pytest + playwright)
│   │   ├── conftest.py   # 共享的playwright夹具
│   │   ├── pages/        # Page Object 类
│   │   │   └── login_page.py
│   │   └── test_login_flow.py
│   └── conftest.py       # 全局共享的pytest夹具(如数据库连接)
├── requirements-test.txt  # 测试依赖
└── pytest.ini            # pytest配置文件

7.2 持续集成(CI)集成

自动化测试只有在持续集成中自动运行才有最大价值。以GitHub Actions为例,一个简单的 .github/workflows/test.yml 配置如下:

name: Python Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.9", "3.10"]

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install -r requirements-test.txt
        playwright install --with-deps chromium  # 安装Playwright及浏览器
    - name: Run unit & integration tests
      run: |
        pytest tests/unit tests/integration -v
    - name: Run E2E tests
      run: |
        pytest tests/e2e --headless -v

这样,每次代码推送或发起拉取请求时,都会自动运行全套测试,确保新代码不会破坏现有功能。

7.3 最后的忠告:避免常见误区

  1. 不要为了自动化而自动化 :自动化测试的投入是有成本的。优先自动化那些 稳定、核心、高频 的业务流程。变化过于频繁的页面不适合做UI自动化。
  2. 测试数据管理 :测试数据是另一个大坑。尽量让测试自己创建所需数据(setup),并在测试后清理(teardown)。使用工厂模式或夹具来生成测试数据。避免使用生产数据库或共享的测试数据库,以免测试间相互干扰。
  3. 测试的独立性 :每个测试用例必须能够独立运行,且不依赖其他测试用例产生的状态或数据。这是保证测试稳定可靠的基本原则。
  4. 失败分析 :当UI测试失败时,不要只看断言错误。第一时间查看自动截屏(Playwright和Selenium都支持)、页面源代码、以及浏览器控制台日志。很多前端错误会在控制台体现。
  5. 保持耐心 :UI自动化测试,尤其是初期,可能会比较“脆弱”。需要花时间优化选择器、完善等待策略、重构Page Object。这是一个迭代的过程,随着框架和经验的成熟,稳定性会越来越高。

unittest 的严谨,到 pytest 的强大,再到 Selenium 的经典和 Playwright 的现代,这四个框架构成了Python自动化测试的坚实拼图。没有哪个是“最好”的,只有“最适合”你当前场景的。我的建议是,从 pytest 开始构建你的单元测试基础,然后用 Playwright 攻克UI测试的难关。记住,工具是为人服务的,清晰的测试思路、良好的代码结构和持续的重构,比单纯追求某个框架的新特性更重要。

更多推荐