Pytest的基本语法与特性:让你的测试更清晰、更强大(Pytest系列之二)

在上篇文章中,我们初步认识了 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 ==============================
跳过 (Skipping) 测试:灵活地控制执行

在某些情况下,你可能需要暂时跳过一些测试用例,例如:

  • 测试功能尚未完成。
  • 测试依赖的环境未配置好。
  • 已知该测试用例会失败,暂时忽略。

Pytest提供了 @pytest.mark.skip@pytest.mark.skipif 装饰器来实现跳过测试。

  • @pytest.mark.skip(reason="..."): 无条件跳过测试,并提供跳过原因。
  • @pytest.mark.skipif(condition, reason="..."): 当 conditionTrue 时跳过测试,并提供跳过原因。
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 =========================
标记 (Marking) 测试:为测试添加元数据

标记 (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 ======================================== 


预期失败 (Expected Failures):标记已知问题

有时候,你可能知道某个测试用例目前会失败,但你仍然希望它在测试报告中有所体现,而不是被完全忽略。这时可以使用 @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 和参数化测试,让你的测试能力更上一层楼!

你可能感兴趣的:(Pytest,pytest)