pytest 语法与核心概念
- Part 1: pytest 语法与核心概念
- 1.1 基础语法
- 1.2 配置文件 (pytest.ini, pyproject.toml, setup.cfg)
- Part 2: pytest 装饰器详解与样例
- 2.1 `@pytest.fixture` - 核心依赖注入与资源管理
- 2.2 `@pytest.mark` - 标记与控制
- 2.3 `@pytest.mark.parametrize` - 数据驱动测试
- Part 3: Playwright 页面元素选择器详解与样例
- 3.1 Locator API (推荐方式)
- 3.2 Locator 对象的链式操作与过滤
- 3.3 智能等待 (Implicit Waits)
- 总结
Part 1: pytest 语法与核心概念
1.1 基础语法
- 测试函数命名:Pytest 会自动发现并运行以
test_开头的函数或_test结尾的函数。deftest_addition():assert1+1==2deftest_string_contains():assert"hello"in"hello world" - 断言:直接使用 Python 的
assert语句。Pytest 会捕获失败的断言并提供清晰的错误信息。deftest_list_length():my_list=[1,2,3]assertlen(my_list)==3assertlen(my_list)>5# This will fail with a clear message
1.2 配置文件 (pytest.ini, pyproject.toml, setup.cfg)
可以用来配置 pytest 的默认行为、添加插件、定义标记等。
pytest.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 *.eggPart 2: pytest 装饰器详解与样例
2.1@pytest.fixture- 核心依赖注入与资源管理
作用:提供测试所需的数据或对象,并管理它们的创建(setup)和销毁(teardown)。
基本用法:
importpytest@pytest.fixturedefsample_data():"""提供一个简单的数据示例."""data={"key":"value","number":42}returndatadeftest_use_sample_data(sample_data):"""使用 fixture 提供的数据."""assertsample_data["key"]=="value"assertsample_data["number"]==42yield与 teardown:
importtempfileimportos@pytest.fixturedeftemp_file_path():"""创建临时文件,并在测试后删除."""temp_dir=tempfile.mkdtemp()temp_file=os.path.join(temp_dir,"temp.txt")withopen(temp_file,'w')asf:f.write("Test content")yieldtemp_file# 返回文件路径给测试函数# Teardown: 测试函数执行完毕后,清理临时文件os.remove(temp_file)os.rmdir(temp_dir)deftest_read_temp_file(temp_file_path):withopen(temp_file_path,'r')asf:content=f.read()assertcontent=="Test content"scope参数:
@pytest.fixture(scope="session")# 整个测试会话期间只执行一次defexpensive_resource():print("\n--- Creating expensive resource (e.g., database connection) ---")resource=create_expensive_resource()yieldresourceprint("\n--- Cleaning up expensive resource ---")destroy_resource(resource)@pytest.fixture(scope="function")# 每个测试函数执行一次 (默认)defper_test_setup():print("\n--- Per-test setup ---")yieldprint("\n--- Per-test teardown ---")@pytest.fixture(scope="class")# 每个测试类执行一次defper_class_data():print("\n--- Per-class setup ---")data=load_class_data()yielddataprint("\n--- Per-class teardown ---")# Test functions using fixturesdeftest_a(expensive_resource,per_test_setup):assertTruedeftest_b(expensive_resource,per_test_setup):assertTrueautouse=True:
@pytest.fixture(scope="function",autouse=True)# 自动应用于所有函数级测试defauto_log():print("\n[LOG] Starting a test function")yieldprint("[LOG] Finished a test function")deftest_one():# 不需要显式声明 auto_logassertTruedeftest_two():# 不需要显式声明 auto_logassert1==1params参数 (与parametrize类似):
@pytest.fixture(params=[1,2,3],ids=["one","two","three"])# ids 提供更清晰的测试名称defnumber_fixture(request):returnrequest.paramdeftest_number_properties(number_fixture):assertnumber_fixture>0# 这个测试会被执行 3 次,分别传入 1, 2, 32.2@pytest.mark- 标记与控制
@pytest.mark.skip和@pytest.mark.skipif:
importsys@pytest.mark.skip(reason="Feature not implemented yet")deftest_incomplete_feature():pass@pytest.mark.skipif(sys.platform=="win32",reason="Does not run on Windows")deftest_unix_specific():# This test will be skipped on Windowspass@pytest.mark.xfail- 预期失败:
@pytest.mark.xfail(reason="Known bug #12345")deftest_known_buggy_functionality():assertbuggy_function()=="expected_result"# 如果返回 "expected_result",则标记为 XPASS# 如果返回其他值或抛出异常,则标记为 XFAIL@pytest.mark.xfail(strict=True,reason="Must fix this bug")# strict=True 意味着如果 XPASS,则测试失败deftest_critical_buggy_functionality():assertcritical_buggy_function()=="fixed_result"自定义标记:
# 在 pytest.ini 中注册标记 (如上所示)@pytest.mark.smokedeftest_smoke_login():assertlogin("user","pass")==True@pytest.mark.ui@pytest.mark.slowdeftest_ui_heavy_scenario():# Complex UI testpass# 运行时过滤:# pytest -m "smoke" # 只运行 smoke 标记的测试# pytest -m "ui and not slow" # 运行 ui 但不运行 slow 标记的测试# pytest -m "not slow" # 运行所有非 slow 标记的测试2.3@pytest.mark.parametrize- 数据驱动测试
基本用法:
@pytest.mark.parametrize("input_val, expected_output",[(2,4),(3,9),(4,16),(-1,1),])deftest_square(input_val,expected_output):assertsquare(input_val)==expected_output# 此测试会运行 4 次defsquare(x):returnx*x多参数与ids:
@pytest.mark.parametrize("username, password, expected_success",[("admin","secret",True),("user","wrongpass",False),("","any",False),],ids=["valid_admin","invalid_pass","empty_user"])deftest_login(username,password,expected_success):result=attempt_login(username,password)assertresult==expected_success# 输出中会显示 test_login[valid_admin], test_login[invalid_pass], etc.defattempt_login(username,password):# Simulate login logicreturnusername=="admin"andpassword=="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 示例:
<formid="loginForm"><labelfor="email">Email Address:</label><inputtype="email"id="email"name="email"placeholder="Enter your email"data-testid="email-input"><labelfor="password">Password:</label><inputtype="password"id="password"name="password"data-testid="password-input"><inputtype="checkbox"id="remember"name="remember"data-testid="remember-checkbox"><labelfor="remember">Remember me</label><inputtype="radio"id="male"name="gender"value="male"data-testid="gender-male"><labelfor="male">Male</label><inputtype="radio"id="female"name="gender"value="female"data-testid="gender-female"><labelfor="female">Female</label><buttontype="submit"data-testid="submit-btn">Log In</button></form>Playwright 样例:
# conftest.py (典型 fixture 设置)importpytestfromplaywright.sync_apiimportsync_playwright@pytest.fixture(scope="session")defbrowser():withsync_playwright()asp:browser=p.chromium.launch(headless=False)# Set headless=True for CIyieldbrowser browser.close()@pytest.fixturedefpage(browser):context=browser.new_context()page=context.new_page()yieldpage context.close()# test_form_interactions.pydeftest_form_filling_and_submission(page):page.goto("https://your-test-site.com/login.html")# Replace with your URL# Fill inputs using various locatorspage.get_by_label("Email Address:").fill("test@example.com")# Using label textpage.get_by_test_id("password-input").fill("SecurePass123!")# Using test-id# Interact with checkboxes and radio buttonsremember_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 stateassertnotremember_checkbox.is_checked()assertnotgender_male_radio.is_checked()assertnotgender_female_radio.is_checked()# Perform actionsremember_checkbox.check()# Check the boxgender_male_radio.check()# Select male radio# Verify state after actionassertremember_checkbox.is_checked()assertgender_male_radio.is_checked()assertnotgender_female_radio.is_checked()# Female should be unchecked now# Submit the formpage.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()deftest_element_visibility_and_interaction(page):page.goto("https://your-test-site.com/some-page.html")# Wait for an element to be visible before interactingbutton=page.get_by_role("button",name="Load More")button.wait_for(state="visible")# Explicit wait if needed, though often automaticbutton.click()# Use text-based locatorstatus_message=page.get_by_text("Loading...")# Playwright waits for this element to appearassertstatus_message.is_visible()# Use CSS selector if necessary (though less preferred)loading_spinner=page.locator(".spinner")# CSS class# Wait for it to disappearloading_spinner.wait_for(state="detached")# Verify final statenew_content=page.get_by_test_id("loaded-content")assertnew_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"),])deftest_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 messagefromplaywright.sync_apiimportexpect message_element=page.get_by_test_id("message")# Assume there's a message divexpect(message_element).to_contain_text(expected_message)3.2 Locator 对象的链式操作与过滤
deftest_locating_within_containers(page):page.goto("https://your-test-site.com/products.html")# Locate a container firstproduct_list=page.locator("#product-list")# Then find elements within that containerfirst_product_name=product_list.locator(".product-name").firstassertfirst_product_name.text_content()=="Product A"# Filter locators based on inner text or other conditionsdelete_buttons=page.get_by_role("button",name="Delete")# Find the delete button for a specific product nametarget_delete_button=delete_buttons.filter(has_text="Product B").first target_delete_button.click()# Get nth elementthird_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()等)都会自动等待元素达到所需状态。
deftest_smart_waiting(page):page.goto("https://your-test-site.com/delayed-content.html")# Click a button that triggers async content loadingpage.get_by_test_id("load-data-btn").click()# No need for time.sleep()!# The following line waits automatically for the element to become visibledynamic_content=page.get_by_test_id("dynamic-content")assertdynamic_content.is_visible()assert"Loaded"indynamic_content.text_content()# Using expect for assertions also waits implicitlyfromplaywright.sync_apiimportexpect expect(dynamic_content).to_contain_text("Data Loaded Successfully")总结
- pytest:通过
fixture管理资源和依赖,通过mark控制测试行为,通过parametrize实现数据驱动。 - Playwright:利用
get_by_*等语义化 Locator API 定位元素,利用内置等待机制简化测试代码,使测试更稳定、更易读。