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- 数据驱动测试)
- [2.1 `@pytest.fixture` - 核心依赖注入与资源管理](#2.1
- [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结尾的函数。pythondef test_addition(): assert 1 + 1 == 2 def test_string_contains(): assert "hello" in "hello world" -
断言 :直接使用 Python 的
assert语句。Pytest 会捕获失败的断言并提供清晰的错误信息。pythondef 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 定位元素,利用内置等待机制简化测试代码,使测试更稳定、更易读。