【自动化测试】pytest 语法与核心概念

pytest 语法与核心概念

    • [Part 1: pytest 语法与核心概念](#Part 1: pytest 语法与核心概念)
      • [1.1 基础语法](#1.1 基础语法)
      • [1.2 配置文件 (pytest.ini, pyproject.toml, setup.cfg)](#1.2 配置文件 (pytest.ini, pyproject.toml, setup.cfg))
    • [Part 2: pytest 装饰器详解与样例](#Part 2: pytest 装饰器详解与样例)
      • [2.1 `@pytest.fixture` - 核心依赖注入与资源管理](#2.1 @pytest.fixture - 核心依赖注入与资源管理)
      • [2.2 `@pytest.mark` - 标记与控制](#2.2 @pytest.mark - 标记与控制)
      • [2.3 `@pytest.mark.parametrize` - 数据驱动测试](#2.3 @pytest.mark.parametrize - 数据驱动测试)
    • [Part 3: Playwright 页面元素选择器详解与样例](#Part 3: Playwright 页面元素选择器详解与样例)
      • [3.1 Locator API (推荐方式)](#3.1 Locator API (推荐方式))
      • [3.2 Locator 对象的链式操作与过滤](#3.2 Locator 对象的链式操作与过滤)
      • [3.3 智能等待 (Implicit Waits)](#3.3 智能等待 (Implicit Waits))
    • 总结

Part 1: pytest 语法与核心概念

1.1 基础语法

  • 测试函数命名 :Pytest 会自动发现并运行以 test_ 开头的函数或 _test 结尾的函数。

    python 复制代码
    def test_addition():
        assert 1 + 1 == 2
    
    def test_string_contains():
        assert "hello" in "hello world"
  • 断言 :直接使用 Python 的 assert 语句。Pytest 会捕获失败的断言并提供清晰的错误信息。

    python 复制代码
    def test_list_length():
        my_list = [1, 2, 3]
        assert len(my_list) == 3
        assert len(my_list) > 5 # This will fail with a clear message

1.2 配置文件 (pytest.ini, pyproject.toml, setup.cfg)

可以用来配置 pytest 的默认行为、添加插件、定义标记等。

pytest.ini 示例

ini 复制代码
[tool:pytest]
# 标记注册,方便 IDE 识别
markers =
    smoke: marks tests as part of the smoke suite
    ui: marks tests related to UI interactions
    slow: marks tests as slow
# 默认命令行参数
addopts = -v -x --tb=short
# 指定测试路径
testpaths = tests
# 忽略某些路径
norecursedirs = .git dist build *.egg

Part 2: pytest 装饰器详解与样例

2.1 @pytest.fixture - 核心依赖注入与资源管理

作用:提供测试所需的数据或对象,并管理它们的创建(setup)和销毁(teardown)。

基本用法

python 复制代码
import pytest

@pytest.fixture
def sample_data():
    """提供一个简单的数据示例."""
    data = {"key": "value", "number": 42}
    return data

def test_use_sample_data(sample_data):
    """使用 fixture 提供的数据."""
    assert sample_data["key"] == "value"
    assert sample_data["number"] == 42

yield 与 teardown

python 复制代码
import tempfile
import os

@pytest.fixture
def temp_file_path():
    """创建临时文件,并在测试后删除."""
    temp_dir = tempfile.mkdtemp()
    temp_file = os.path.join(temp_dir, "temp.txt")
    with open(temp_file, 'w') as f:
        f.write("Test content")
    yield temp_file # 返回文件路径给测试函数
    # Teardown: 测试函数执行完毕后,清理临时文件
    os.remove(temp_file)
    os.rmdir(temp_dir)

def test_read_temp_file(temp_file_path):
    with open(temp_file_path, 'r') as f:
        content = f.read()
    assert content == "Test content"

scope 参数

python 复制代码
@pytest.fixture(scope="session") # 整个测试会话期间只执行一次
def expensive_resource():
    print("\n--- Creating expensive resource (e.g., database connection) ---")
    resource = create_expensive_resource()
    yield resource
    print("\n--- Cleaning up expensive resource ---")
    destroy_resource(resource)

@pytest.fixture(scope="function") # 每个测试函数执行一次 (默认)
def per_test_setup():
    print("\n--- Per-test setup ---")
    yield
    print("\n--- Per-test teardown ---")

@pytest.fixture(scope="class") # 每个测试类执行一次
def per_class_data():
    print("\n--- Per-class setup ---")
    data = load_class_data()
    yield data
    print("\n--- Per-class teardown ---")

# Test functions using fixtures
def test_a(expensive_resource, per_test_setup):
    assert True

def test_b(expensive_resource, per_test_setup):
    assert True

autouse=True

python 复制代码
@pytest.fixture(scope="function", autouse=True) # 自动应用于所有函数级测试
def auto_log():
    print("\n[LOG] Starting a test function")
    yield
    print("[LOG] Finished a test function")

def test_one(): # 不需要显式声明 auto_log
    assert True

def test_two(): # 不需要显式声明 auto_log
    assert 1 == 1

params 参数 (与 parametrize 类似)

python 复制代码
@pytest.fixture(params=[1, 2, 3], ids=["one", "two", "three"]) # ids 提供更清晰的测试名称
def number_fixture(request):
    return request.param

def test_number_properties(number_fixture):
    assert number_fixture > 0
    # 这个测试会被执行 3 次,分别传入 1, 2, 3

2.2 @pytest.mark - 标记与控制

@pytest.mark.skip@pytest.mark.skipif

python 复制代码
import sys

@pytest.mark.skip(reason="Feature not implemented yet")
def test_incomplete_feature():
    pass

@pytest.mark.skipif(sys.platform == "win32", reason="Does not run on Windows")
def test_unix_specific():
    # This test will be skipped on Windows
    pass

@pytest.mark.xfail - 预期失败

python 复制代码
@pytest.mark.xfail(reason="Known bug #12345")
def test_known_buggy_functionality():
    assert buggy_function() == "expected_result" # 如果返回 "expected_result",则标记为 XPASS
    # 如果返回其他值或抛出异常,则标记为 XFAIL

@pytest.mark.xfail(strict=True, reason="Must fix this bug") # strict=True 意味着如果 XPASS,则测试失败
def test_critical_buggy_functionality():
    assert critical_buggy_function() == "fixed_result"

自定义标记

python 复制代码
# 在 pytest.ini 中注册标记 (如上所示)
@pytest.mark.smoke
def test_smoke_login():
    assert login("user", "pass") == True

@pytest.mark.ui
@pytest.mark.slow
def test_ui_heavy_scenario():
    # Complex UI test
    pass

# 运行时过滤:
# pytest -m "smoke"         # 只运行 smoke 标记的测试
# pytest -m "ui and not slow" # 运行 ui 但不运行 slow 标记的测试
# pytest -m "not slow"      # 运行所有非 slow 标记的测试

2.3 @pytest.mark.parametrize - 数据驱动测试

基本用法

python 复制代码
@pytest.mark.parametrize("input_val, expected_output", [
    (2, 4),
    (3, 9),
    (4, 16),
    (-1, 1),
])
def test_square(input_val, expected_output):
    assert square(input_val) == expected_output
    # 此测试会运行 4 次

def square(x):
    return x * x

多参数与 ids

python 复制代码
@pytest.mark.parametrize("username, password, expected_success", [
    ("admin", "secret", True),
    ("user", "wrongpass", False),
    ("", "any", False),
], ids=["valid_admin", "invalid_pass", "empty_user"])
def test_login(username, password, expected_success):
    result = attempt_login(username, password)
    assert result == expected_success
    # 输出中会显示 test_login[valid_admin], test_login[invalid_pass], etc.

def attempt_login(username, password):
    # Simulate login logic
    return username == "admin" and password == "secret"

Part 3: Playwright 页面元素选择器详解与样例

3.1 Locator API (推荐方式)

这些方法基于用户可见性语义化,更稳定。

方法 说明 样例
page.get_by_role(role, name=...) 按 ARIA 角色定位,如 button, link, textbox, checkbox, radio. page.get_by_role("button", name="Submit")
page.get_by_text(text) 按元素内部可见文本定位。 page.get_by_text("Sign In").click()
page.get_by_label(text) 按关联的 <label> 标签文本定位输入框等。 page.get_by_label("Email:").fill("user@example.com")
page.get_by_placeholder(text) placeholder 属性定位。 page.get_by_placeholder("Search...").fill("query")
page.get_by_alt_text(text) 按图片的 alt 属性定位。 page.get_by_alt_text("Logo").click()
page.get_by_title(text) title 属性定位。 page.get_by_title("Help").click()
page.get_by_test_id(test_id_value) data-testid 属性定位(最推荐用于测试)。 page.get_by_test_id("submit-btn").click()

HTML 示例

html 复制代码
<form id="loginForm">
  <label for="email">Email Address:</label>
  <input type="email" id="email" name="email" placeholder="Enter your email" data-testid="email-input">

  <label for="password">Password:</label>
  <input type="password" id="password" name="password" data-testid="password-input">

  <input type="checkbox" id="remember" name="remember" data-testid="remember-checkbox">
  <label for="remember">Remember me</label>

  <input type="radio" id="male" name="gender" value="male" data-testid="gender-male">
  <label for="male">Male</label>
  <input type="radio" id="female" name="gender" value="female" data-testid="gender-female">
  <label for="female">Female</label>

  <button type="submit" data-testid="submit-btn">Log In</button>
</form>

Playwright 样例

python 复制代码
# conftest.py (典型 fixture 设置)
import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False) # Set headless=True for CI
        yield browser
        browser.close()

@pytest.fixture
def page(browser):
    context = browser.new_context()
    page = context.new_page()
    yield page
    context.close()

# test_form_interactions.py
def test_form_filling_and_submission(page):
    page.goto("https://your-test-site.com/login.html") # Replace with your URL

    # Fill inputs using various locators
    page.get_by_label("Email Address:").fill("test@example.com") # Using label text
    page.get_by_test_id("password-input").fill("SecurePass123!") # Using test-id

    # Interact with checkboxes and radio buttons
    remember_checkbox = page.get_by_test_id("remember-checkbox")
    gender_male_radio = page.get_by_test_id("gender-male")
    gender_female_radio = page.get_by_test_id("gender-female")

    # Check initial state
    assert not remember_checkbox.is_checked()
    assert not gender_male_radio.is_checked()
    assert not gender_female_radio.is_checked()

    # Perform actions
    remember_checkbox.check() # Check the box
    gender_male_radio.check() # Select male radio

    # Verify state after action
    assert remember_checkbox.is_checked()
    assert gender_male_radio.is_checked()
    assert not gender_female_radio.is_checked() # Female should be unchecked now

    # Submit the form
    page.get_by_test_id("submit-btn").click()

    # Wait for navigation or specific element after submission
    # Example: expect(page).to_have_url("/dashboard")
    # Example: expect(page.get_by_test_id("welcome-message")).to_be_visible()

def test_element_visibility_and_interaction(page):
    page.goto("https://your-test-site.com/some-page.html")

    # Wait for an element to be visible before interacting
    button = page.get_by_role("button", name="Load More")
    button.wait_for(state="visible") # Explicit wait if needed, though often automatic
    button.click()

    # Use text-based locator
    status_message = page.get_by_text("Loading...")
    # Playwright waits for this element to appear
    assert status_message.is_visible()

    # Use CSS selector if necessary (though less preferred)
    loading_spinner = page.locator(".spinner") # CSS class
    # Wait for it to disappear
    loading_spinner.wait_for(state="detached")

    # Verify final state
    new_content = page.get_by_test_id("loaded-content")
    assert new_content.is_visible()

# test_with_parametrization.py
@pytest.mark.parametrize("username, password, expected_message", [
    ("valid_user", "valid_pass", "Welcome"),
    ("invalid_user", "wrong_pass", "Invalid credentials"),
    ("", "any_pass", "Username is required"),
])
def test_login_scenarios(page, username, password, expected_message):
    page.goto("https://your-test-site.com/login.html")

    page.get_by_test_id("username-input").fill(username)
    page.get_by_test_id("password-input").fill(password)
    page.get_by_test_id("submit-btn").click()

    # Wait for and assert error/success message
    from playwright.sync_api import expect
    message_element = page.get_by_test_id("message") # Assume there's a message div
    expect(message_element).to_contain_text(expected_message)

3.2 Locator 对象的链式操作与过滤

python 复制代码
def test_locating_within_containers(page):
    page.goto("https://your-test-site.com/products.html")

    # Locate a container first
    product_list = page.locator("#product-list")

    # Then find elements within that container
    first_product_name = product_list.locator(".product-name").first
    assert first_product_name.text_content() == "Product A"

    # Filter locators based on inner text or other conditions
    delete_buttons = page.get_by_role("button", name="Delete")
    # Find the delete button for a specific product name
    target_delete_button = delete_buttons.filter(has_text="Product B").first
    target_delete_button.click()

    # Get nth element
    third_item = page.locator(".list-item").nth(2) # Gets the 3rd item (0-indexed)
    third_item.click()

3.3 智能等待 (Implicit Waits)

Playwright 的一大优点是内置了智能等待机制。大多数操作(如 .click(), .fill(), .is_visible() 等)都会自动等待元素达到所需状态。

python 复制代码
def test_smart_waiting(page):
    page.goto("https://your-test-site.com/delayed-content.html")

    # Click a button that triggers async content loading
    page.get_by_test_id("load-data-btn").click()

    # No need for time.sleep()!
    # The following line waits automatically for the element to become visible
    dynamic_content = page.get_by_test_id("dynamic-content")
    assert dynamic_content.is_visible()
    assert "Loaded" in dynamic_content.text_content()

    # Using expect for assertions also waits implicitly
    from playwright.sync_api import expect
    expect(dynamic_content).to_contain_text("Data Loaded Successfully")

总结

  • pytest :通过 fixture 管理资源和依赖,通过 mark 控制测试行为,通过 parametrize 实现数据驱动。
  • Playwright :利用 get_by_* 等语义化 Locator API 定位元素,利用内置等待机制简化测试代码,使测试更稳定、更易读。
相关推荐
zUlKyyRC3 小时前
基于一阶RC模型,FFRLS+EKF算法的电池SOC在线联合估计Matlab程序
pytest
天才测试猿1 天前
Chrome浏览器+Postman做接口测试
自动化测试·软件测试·python·测试工具·测试用例·接口测试·postman
软件测试君1 天前
2025年10款王炸AI测试工具,你用过几款?
自动化测试·软件测试·人工智能·深度学习·测试工具·单元测试·ai测试工具
测开小林1 天前
加入L-Tester开源项目:自动化测试平台
自动化测试·测试开发·开源·fastapi·测试平台
我的xiaodoujiao2 天前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 40--完善优化 Allure 测试报告显示内容
python·学习·测试工具·pytest
我的xiaodoujiao2 天前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 41--自定义定制化展示 Allure 测试报告内容
python·学习·测试工具·pytest
nvd112 天前
Pytest 异步数据库测试实战:基于 AsyncMock 的无副作用打桩方案
数据库·pytest
nvd113 天前
深入分析:Pytest异步测试中的数据库会话事件循环问题
数据库·pytest
爱学习的潇潇3 天前
Postman学习之常用断言
自动化测试·软件测试·功能测试·学习·程序人生·lua·postman