在上篇文章中,我们初步认识了 Pytest
并成功搭建了环境。现在,我们将深入学习 Pytest
的基本语法和一些核心特性,让你能够编写出更清晰、更强大的自动化测试用例。
Pytest
可以识别test开头、结尾的文件
test_*.py
*_test.py
同时它还可以识别用例
Test*类包含的所有test_*方法(类中不能带有__init__方法)
不在class中的所有的test_*.方法
话不多说,下面就让我们实际操作一番
Pytest
的一大亮点就是其测试函数的编写方式。你无需像 unittest
那样继承任何类,只需要定义普通的 Python 函数,并遵循 test_*
的命名约定即可。
def test_addition():
assert 2 + 2 == 4
def test_string_length():
text = "hello"
assert len(text) == 5
def test_say_hello():
assert "Hello" == "hello"
def check_value(data, value):
assert value in data
def test_check_value():
list_check = [1, 23, 3]
check_value(list_check, 23)
def dictionary_value_test():
my_dict = {"name": "Alice", "age": 30}
assert my_dict["age"] == 30
在这个例子中,Pytest
会自动发现并执行它们。[除了最后一个dictionary_value_test(),因为它不是test开头的]
Pytest
沿用了 Python 内置的 assert
语句进行断言。当断言失败时,Pytest
会提供详细的错误信息,帮助你快速定位问题。
============================= test session starts =============================
collecting ... collected 5 items
01_example_test.py::test_addition PASSED [ 20%]
01_example_test.py::test_string_length PASSED [ 40%]
01_example_test.py::test_say_hello FAILED [ 60%]
01_example_test.py:16 (test_say_hello)
'Hello' != 'hello'
Expected :'hello'
Actual :'Hello'
def test_say_hello():
> assert "Hello" == "hello"
E AssertionError: assert 'Hello' == 'hello'
E
E - hello
E ? ^
E + Hello
E ? ^
01_example_test.py:18: AssertionError
01_example_test.py::test_check_value PASSED [ 80%]
01_example_test.py::test_dictionary_value PASSED [100%]
========================= 1 failed, 4 passed in 0.06s =========================
虽然 Pytest
鼓励使用简单的函数进行测试,但对于组织相关的测试用例,你可以使用以 Test
开头的类。类中的测试方法同样需要以 test_
开头。
def test_not_class():
print("我不在类中,但是我是test开头的")
assert True
def not_class_test():
print("我不在类中,但是我是test结尾的")
assert True
class Test_Example_str:
def test_upper(self):
assert "hello".upper() == "HELLO"
def test_spilt(self):
s = "Hello world"
assert s.split() == ["Hello", "world"]
def list_test(self):
assert True
class Example2_list_Test:
def test_append(self):
my_list = [1, 2, 3]
my_list.append(4)
assert my_list == [1, 2, 3]
def test_pop(self):
my_list = [1, 2, 3]
assert my_list.pop() == 3
从输出结果可以看到,之识别并运行了三个用例;不在类中,但是test开头的方法;test开头的类,里面的test开头的方法
使用测试类可以更好地组织相关的测试用例,提高代码的可维护性。
============================= test session starts =============================
collecting ... collected 3 items
02_class_example_test.py::test_not_class PASSED [ 33%]我不在类中,但是我是test开头的
02_class_example_test.py::Test_Example_str::test_upper PASSED [ 66%]
02_class_example_test.py::Test_Example_str::test_spilt PASSED [100%]
============================== 3 passed in 0.01s ==============================
在某些情况下,你可能需要暂时跳过一些测试用例,例如:
Pytest
提供了 @pytest.mark.skip
和 @pytest.mark.skipif
装饰器来实现跳过测试。
@pytest.mark.skip(reason="...")
: 无条件跳过测试,并提供跳过原因。@pytest.mark.skipif(condition, reason="...")
: 当 condition
为 True
时跳过测试,并提供跳过原因。class Test_Example2_list:
def test_append(self):
my_list = [1, 2, 3]
my_list.append(4)
assert my_list == [1, 2, 3]
def test_pop(self):
my_list = [1, 2, 3]
assert my_list.pop() == 3
可以直观的看到,append是一个错误的用例;但是我们可以让它跳过去;这样就不会影响这个list测试类的执行
import pytest
class Test_Example2_list:
# 无条件跳过测试,并提供跳过原因。
@pytest.mark.skip(reason=f"这是个练习")
def test_append(self):
my_list = [1, 2, 3]
my_list.append(4)
assert my_list == [1, 2, 3]
def test_pop(self):
my_list = [1, 2, 3]
assert my_list.pop() == 3
from datetime import datetime
now = datetime.now().day
# 当 `condition` 为 `True` 时跳过测试,并提供跳过原因。
@pytest.mark.skipif(condition = now == 13, reason="当前是13号,所以跳过")
def test_list(self):
assert True
当你运行包含跳过测试的用例时候,可以看一下输出;可以看到,pytest会自动的识别到,这是一个需要跳过的用例,以及跳过的原因
============================= test session starts =============================
collecting ... collected 3 items
02_class_example_test.py::Test_Example2_list::test_append SKIPPED [ 33%]
Skipped: 这是个练习
02_class_example_test.py::Test_Example2_list::test_pop PASSED [ 66%]
02_class_example_test.py::Test_Example2_list::test_list SKIPPED [100%]
Skipped: 当前是13号,所以跳过
======================== 1 passed, 2 skipped in 0.01s =========================
标记 (Marks) 是 Pytest
提供的一种为测试函数或类添加元数据的方式。你可以使用内置的标记或自定义标记来组织和控制测试的执行。
内置标记:
我们已经接触过 @pytest.mark.skip
和 @pytest.mark.skipif
。其他常用的内置标记包括:
@pytest.mark.parametrize
: 用于参数化测试 (将在后续文章中详细介绍)。@pytest.mark.xfail
: 用于标记预期失败的测试 (将在后续文章中介绍)。自定义标记:
你可以通过在 pytest.ini
文件中注册自定义标记,以便更好地组织你的测试用例。
# pytest.ini
[pytest]
markers =
smoke: mark tests as smoke tests
regression: mark tests as regression tests
然后在你的测试用例中使用自定义标记:
import pytest
@pytest.mark.smoke
def test_login_functionality():
assert True
@pytest.mark.regression
def test_checkout_process():
assert True
@pytest.mark.regression
@pytest.mark.smoke
def test_user_profile_update():
assert True
def test_none():
assert True
你可以使用 -m
命令行选项来运行带有特定标记的测试:
pytest .\03_mark_example.py -m "smoke" # 只运行标记为 smoke 的测试
输出,可以看到总共4条用例,2条运行
============================================== test session starts ==============================================
platform win32 -- Python 3.13.2, pytest-8.3.5, pluggy-1.5.0
rootdir: D:\Code_Study\Python_Pytest
configfile: pytest.ini
collected 4 items / 2 deselected / 2 selected
03_mark_example.py .. [100%]
======================================== 2 passed, 2 deselected in 0.01s ========================================
pytest .\03_mark_example.py -m "not smoke" # 运行所有未标记为 smoke 的测试
输出,可以看到总共4条用例,2条运行
============================================== test session starts ==============================================
platform win32 -- Python 3.13.2, pytest-8.3.5, pluggy-1.5.0
rootdir: D:\Code_Study\Python_Pytest
configfile: pytest.ini
collected 4 items / 2 deselected / 2 selected
03_mark_example.py .. [100%]
======================================== 2 passed, 2 deselected in 0.01s ========================================
pytest .\03_mark_example.py -m "smoke and regression" # 运行同时标记为 smoke 和 regression 的测试
可以看到1条成功
============================================== test session starts ==============================================
platform win32 -- Python 3.13.2, pytest-8.3.5, pluggy-1.5.0
rootdir: D:\Code_Study\Python_Pytest
configfile: pytest.ini
collected 4 items / 3 deselected / 1 selected
03_mark_example.py . [100%]
======================================== 1 passed, 3 deselected in 0.01s ========================================
pytest .\03_mark_example.py -m "smoke or regression" # 运行标记为 smoke 或 regression 的测试
可以看到3条成功运行
============================================== test session starts ==============================================
platform win32 -- Python 3.13.2, pytest-8.3.5, pluggy-1.5.0
rootdir: D:\Code_Study\Python_Pytest
configfile: pytest.ini
collected 4 items / 1 deselected / 3 selected
03_mark_example.py ... [100%]
======================================== 3 passed, 1 deselected in 0.01s ========================================
有时候,你可能知道某个测试用例目前会失败,但你仍然希望它在测试报告中有所体现,而不是被完全忽略。这时可以使用 @pytest.mark.xfail
装饰器。
Python
import pytest
@pytest.mark.xfail(reason="Known bug in the current release")
def test_broken_feature():
assert 1 + 1 == 3 # This test is expected to fail
def test_another_feature():
assert True
输出如下
============================= test session starts =============================
collecting ... collected 2 items
04_xfail_example_test.py::test_add XFAIL (故意失败的) [ 50%]
@pytest.mark.xfail(reason="故意失败的")
def test_add():
> assert 1 + 1 == 3
E assert (1 + 1) == 3
04_xfail_example_test.py:12: AssertionError
04_xfail_example_test.py::test_str PASSED [100%]
======================== 1 passed, 1 xfailed in 0.05s =========================
当被 @pytest.mark.xfail
标记的测试用例失败时,Pytest
会将其标记为 "xfailed"
(预期失败)。如果该测试用例意外通过了,Pytest
会将其标记为 "xpassed"
(意外通过),这可能意味着该 bug 已经修复,你需要重新评估该测试用例。
============================= test session starts =============================
collecting ... collected 2 items
04_xfail_example_test.py::test_add XPASS (故意失败的) [ 50%]
04_xfail_example_test.py::test_str PASSED [100%]
======================== 1 passed, 1 xpassed in 0.01s =========================
总结
在本篇文章中,我们学习了 Pytest
的基本语法和一些核心特性,包括:
assert
断言。@pytest.mark.skip
, @pytest.mark.skipif
).@pytest.mark.*
),用于组织和控制测试执行。@pytest.mark.xfail
标记预期失败的测试。掌握这些基本语法和特性,你已经可以编写出更结构化、更易于管理和更具控制性的 Pytest
自动化测试用例了。在接下来的文章中,我们将继续深入学习 Pytest
的更多高级功能,例如 Fixture 和参数化测试,让你的测试能力更上一层楼!