news 2026/6/24 11:34:46

Python自动化测试入门:手把手创建第一个pytest测试案例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python自动化测试入门:手把手创建第一个pytest测试案例

1. 项目概述:为什么从pytest开始你的测试之旅?

如果你刚开始接触Python自动化测试,或者厌倦了unittest那略显繁琐的写法,那么pytest绝对是你应该立刻上手的神器。它不是什么遥不可及的高深框架,而是一个让写测试变得像写普通Python代码一样简单的工具。网上很多教程一上来就讲fixture、参数化、插件,把新手直接劝退。今天我们不搞那些复杂的,就从最核心、最本质的地方开始:手把手创建一个最简单的pytest测试案例。这个案例将是你测试大厦的第一块砖,理解了它,后面所有的高级特性都是在这个基础上的自然延伸。

pytest的魅力在于它的“约定优于配置”。你不需要继承某个特定的类,不需要写一堆setUptearDown方法,只要你的函数名以test_开头,或者类名以Test开头且其中的方法以test_开头,pytest就能自动发现并运行它们。这种极简的哲学,让开发者可以更专注于测试逻辑本身,而不是框架的条条框框。我们接下来要做的,就是体验这种“极简”带来的畅快感。

2. 环境准备与pytest初体验

2.1 安装pytest:一行命令的事

万事开头难,但安装pytest一点也不难。打开你的终端(Windows上是CMD或PowerShell,Mac/Linux上是Terminal),确保你已经安装了Python(建议3.7及以上版本),然后输入下面这行命令:

pip install pytest

如果网络通畅,几秒钟后pytest就安装好了。你可以通过pytest --version来验证安装是否成功,它会打印出当前的pytest版本号。这里有个新手常踩的坑:如果你电脑上安装了多个Python版本(比如同时有Python 3.8和Python 3.11),要确保你使用的pip命令和后续运行测试的python命令来自同一个Python环境。一个简单的检查方法是运行pip -Vpython -V,看它们指向的Python版本是否一致。不一致的话,你可能需要使用python -m pip install pytest或者指定完整路径的pip来安装。

2.2 创建你的第一个测试文件

安装好之后,我们不需要任何复杂的项目结构。在你电脑的任意位置,新建一个文件夹,比如叫my_first_pytest。然后在这个文件夹里,用你喜欢的代码编辑器(VS Code, PyCharm, 甚至记事本都行)创建一个Python文件。记住pytest的约定:测试文件的名字应该以test_开头,或者以_test.py结尾。这样pytest才能自动识别它。

为了让我们的第一个案例有意义,我们得先有一个被测的功能。我们创建一个非常简单的函数,它负责计算两个数的和。在同一目录下,创建一个名为calculator.py的文件:

# calculator.py def add(a, b): """返回两个数的和""" return a + b

现在,我们来为这个add函数编写测试。创建一个名为test_calculator.py的文件:

# test_calculator.py from calculator import add def test_add_two_positive_numbers(): """测试两个正数相加""" result = add(3, 5) assert result == 8 def test_add_positive_and_negative(): """测试正数与负数相加""" result = add(10, -4) assert result == 6

看,这就是一个最纯粹的pytest测试案例!没有类,没有继承,只有普通的Python函数。函数名以test_开头,这就是告诉pytest:“嘿,我是一个测试用例!” 函数内部,我们调用被测函数add,然后用assert语句来断言结果是否符合预期。assert是Python的关键字,如果后面的表达式为真,则无事发生;如果为假,则会抛出AssertionError异常,pytest会捕获这个异常并将其标记为测试失败。

2.3 运行测试并解读结果

激动人心的时刻到了!打开终端,导航到my_first_pytest目录下,然后直接运行命令:

pytest

你会看到类似下面的输出:

============================= test session starts ============================== platform darwin -- Python 3.9.7, pytest-7.4.0, pluggy-1.2.0 rootdir: /path/to/my_first_pytest collected 2 items test_calculator.py .. [100%] ============================== 2 passed in 0.01s ===============================

这短短几行信息量很大:

  1. 测试会话开始:显示了Python版本、pytest版本等信息。
  2. rootdir:pytest搜索测试文件的根目录。
  3. collected 2 items:pytest自动发现了2个以test_开头的测试函数。
  4. ..:每个点.代表一个通过的测试用例。两个点就是两个都通过了。
  5. [100%]:测试进度条。
  6. 2 passed in 0.01s:最终结果,2个测试全部通过,耗时0.01秒。

如果测试失败了呢?我们来故意写一个错误的断言看看。修改test_add_two_positive_numbers函数中的断言为assert result == 9,再次运行pytest

... (前面部分相同) test_calculator.py F. [ 50%] =================================== FAILURES =================================== _________________________ test_add_two_positive_numbers ________________________ def test_add_two_positive_numbers(): result = add(3, 5) > assert result == 9 E assert 8 == 9 test_calculator.py:5: AssertionError =========================== short test summary info ============================ FAILED test_calculator.py::test_add_two_positive_numbers - assert 8 == 9 ========================= 1 failed, 1 passed in 0.02s ==========================

pytest给出了非常清晰的失败报告:

  • F表示失败(Fail),.表示通过。
  • FAILURES部分详细展示了是哪个测试函数失败了,并指出了出错的具体行(> assert result == 9)以及断言失败的原因(E assert 8 == 9)。
  • short test summary info给出了简明的总结。

这种清晰的报告是pytest广受欢迎的原因之一,它能帮你快速定位问题所在。

3. 核心测试用例设计详解

3.1 理解断言(Assert):测试的基石

在pytest中,assert语句是验证测试结果的核心。你几乎可以在assert后面使用任何返回布尔值的表达式。除了简单的相等判断,还有一些非常实用的断言方式:

# test_assertions.py def test_assertions(): # 相等/不相等 assert 1 + 1 == 2 assert 2 * 2 != 5 # 包含/不包含 (针对列表、字符串等) assert \'hello\' in \'hello world\' assert \'foo\' not in [\'bar\', \'baz\'] # 真假判断 my_list = [] assert not my_list # 断言列表为空(为假) assert my_list is not None # 断言对象不是None # 比较大小 assert 3 < 5 assert 10 >= 10 # 异常断言:使用 pytest.raises import pytest with pytest.raises(ZeroDivisionError): _ = 1 / 0

注意assert语句在Python解释器以优化模式(-O参数)运行时会被全局忽略,这会导致所有测试“凭空通过”。但不用担心,pytest在运行时默认会修改Python的字节码,确保assert语句始终生效,所以你不需要为此做任何特殊处理。

3.2 测试用例的“3A”结构

一个好的测试用例应该结构清晰,遵循“3A”模式:Arrange(准备), Act(执行), Assert(断言)。这能让测试代码更易读、易维护。

让我们用这个模式重构之前的测试:

# test_calculator_refactored.py from calculator import add def test_add_two_positive_numbers(): """测试两个正数相加 - 3A模式""" # 1. Arrange (准备): 设置测试数据和期望结果 a = 3 b = 5 expected_result = 8 # 2. Act (执行): 调用被测功能 actual_result = add(a, b) # 3. Assert (断言): 验证实际结果是否符合预期 assert actual_result == expected_result def test_add_with_zero(): """测试与零相加""" # Arrange a = 7 b = 0 expected = 7 # Act result = add(a, b) # Assert assert result == expected

虽然看起来代码行数变多了,但逻辑层次非常清晰。任何人(包括三个月后的你自己)一眼就能看出这个测试在测什么、数据是什么、预期结果是什么。当测试失败时,这种结构也更容易调试,因为你很容易定位到是准备数据出了问题,还是执行过程有误,或者是预期结果设错了。

3.3 为测试函数添加有意义的文档字符串

你可能注意到了,我在每个测试函数开头都写了用三引号包裹的字符串(docstring)。这不仅仅是注释,它是测试用例的“名片”。当你使用pytest -v(verbose模式)运行测试时,或者在生成测试报告时,这些文档字符串会被显示出来,让你快速了解每个测试的意图。

pytest -v

输出会包含每个测试函数的名称和它的文档字符串,这对于理解测试套件的整体覆盖范围非常有帮助。

4. 组织你的测试:从单个文件到测试目录

4.1 使用测试类(Test Class)分组相关用例

当测试用例越来越多时,全部放在一堆函数里会显得混乱。我们可以使用类来对相关的测试进行逻辑分组。pytest规定,类名必须以Test开头,并且类中的测试方法名以test_开头。

假设我们的计算器功能扩展了,除了加法还有减法。我们可以这样组织:

# test_calculator_class.py from calculator import add, subtract # 假设我们新增了subtract函数 class TestCalculatorAddition: """测试加法功能组""" def test_add_positives(self): assert add(2, 3) == 5 def test_add_negatives(self): assert add(-1, -1) == -2 def test_add_mixed(self): assert add(5, -3) == 2 class TestCalculatorSubtraction: """测试减法功能组""" def test_subtract_positives(self): assert subtract(5, 3) == 2 def test_subtract_negative_result(self): assert subtract(3, 5) == -2

使用类的好处是,你可以将多个测试方法共享的初始化或清理逻辑放在类级别的setup_methodteardown_method中(这是pytest对xUnit风格的支持)。但更pytest的方式是使用fixture,我们稍后会提到。

4.2 创建测试目录(tests/)结构

对于一个真实项目,我们通常不会把测试文件和源代码混在一起。标准的做法是创建一个名为tests的目录,与你的主代码目录并列。

my_project/ ├── calculator.py # 源代码 ├── other_module.py └── tests/ # 测试目录 ├── __init__.py # 让pytest将tests识别为包(可选,但推荐) ├── test_calculator.py ├── test_other.py └── subfolder/ # 你还可以创建子目录来进一步组织测试 └── test_more.py

在这种结构下,你可以在项目根目录(my_project/)直接运行pytest,pytest会自动递归地发现tests目录下所有符合命名规则的测试文件。__init__.py文件的存在告诉Python这个目录是一个包,这能帮助pytest正确地导入你的被测模块。如果你的项目结构复杂(比如源代码在src/目录下),你可能需要配置pytest.ini文件来设置Python路径,但对于简单项目,上面的结构足够了。

4.3 使用conftest.py共享Fixture

这是pytest一个非常强大的特性。conftest.py是一个特殊的文件,pytest会自动发现它,并且其中定义的fixture可以被该文件所在目录及其所有子目录中的测试文件使用,无需显式导入。

假设我们多个测试都需要一个“临时用户”的数据。我们可以在tests/目录下创建一个conftest.py

# tests/conftest.py import pytest @pytest.fixture def sample_user(): """提供一个标准的测试用户数据""" user = { \'name\': \'Test User\', \'age\': 30, \'email\': \'test@example.com\' } return user

然后,在任何tests/目录下的测试文件中,你都可以直接使用sample_user这个fixture,只需在测试函数参数中声明它:

# tests/test_user.py def test_user_name(sample_user): # pytest会自动注入sample_user fixture assert sample_user[\'name\'] == \'Test User\' assert isinstance(sample_user[\'name\'], str) def test_user_age(sample_user): assert 18 <= sample_user[\'age\'] <= 100

fixture的概念是pytest的核心之一,它用于提供测试所需的依赖、设置测试环境、清理测试数据等。@pytest.fixture装饰器标记一个函数为fixture。当测试函数将它列为参数时,pytest会在执行测试前调用这个fixture函数,并将其返回值传递给测试函数。这极大地提升了代码的复用性和可维护性。

5. 进阶技巧与最佳实践

5.1 参数化测试:用一组数据测试多种情况

为同一个测试逻辑编写多个仅数据不同的测试函数非常枯燥。pytest的@pytest.mark.parametrize装饰器完美解决了这个问题。它允许你定义一个测试函数,然后为其提供多组输入数据和期望输出。

让我们用参数化来全面测试加法函数:

# test_calculator_parametrize.py import pytest from calculator import add # 第一个参数是参数字符串,第二个参数是一个列表,列表中的每个元组是一组测试数据 @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), # 正数加正数 (-1, -1, -2), # 负数加负数 (0, 5, 5), # 零加正数 (5, 0, 5), # 正数加零 (-3, 5, 2), # 负数加正数 (1.5, 2.5, 4.0), # 浮点数 ]) def test_add_parametrized(a, b, expected): """使用参数化全面测试add函数""" result = add(a, b) assert result == expected

运行这个测试,pytest会将其展开为6个独立的测试用例来执行,并分别报告结果。如果其中某一组数据失败,报告会明确指出是哪一组(a, b, expected)导致了失败。这比写6个独立的测试函数高效、整洁得多。

5.2 跳过(Skip)和预期失败(XFail)测试

不是所有测试在任何时候都需要运行。有时某些功能尚未实现,或者只在特定环境下有效。pytest提供了装饰器来处理这些情况。

# test_conditional.py import pytest import sys @pytest.mark.skip(reason="此功能在v2.0中尚未实现") def test_new_feature(): assert False # 这个测试不会运行 @pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8或更高版本") def test_feature_requires_py38(): # 只有在Python>=3.8时才会运行此测试 pass @pytest.mark.xfail(reason="已知问题,BUG-123") def test_buggy_feature(): # 这个测试我们预期它会失败 assert some_buggy_function() == \"expected\"
  • @pytest.mark.skip:无条件跳过该测试。
  • @pytest.mark.skipif:在满足条件时跳过。
  • @pytest.mark.xfail:标记测试为“预期失败”。如果测试通过了,会被报告为XPASS(意外通过);如果失败了,则报告为XFAIL(符合预期)。这常用于跟踪已知的Bug。

5.3 使用Fixture进行测试准备和清理

前面我们简单提到了fixture。一个更强大的fixture可以管理资源的生命周期,比如创建数据库连接、临时文件,并在测试结束后自动清理。

# conftest.py 或测试文件中 import pytest import tempfile import os @pytest.fixture def temporary_file(): """创建一个临时文件,测试后自动删除""" # 1. Setup (准备): 创建资源 temp = tempfile.NamedTemporaryFile(mode=\'w+\', delete=False, suffix=\'.txt\') temp.write(\'Initial content\\n\') temp.close() # 关闭文件以便其他操作 file_path = temp.name yield file_path # 2. 将资源提供给测试函数使用 # 3. Teardown (清理): 无论测试成功与否,都会执行 try: os.unlink(file_path) print(f\"Cleaned up temporary file: {file_path}\") except OSError: pass # 文件可能已被删除 def test_write_to_temp_file(temporary_file): """测试向临时文件写入内容""" with open(temporary_file, \'a\') as f: f.write(\'Additional line\\n\') with open(temporary_file, \'r\') as f: content = f.read() assert \'Initial content\' in content assert \'Additional line\' in content # 测试结束后,temporary_file fixture的teardown部分会自动删除文件

yield关键字是fixture支持setup/teardown的关键。yield之前的代码是setup,yield的值会传递给测试函数,yield之后的代码是teardown。pytest保证teardown代码一定会执行,即使测试过程中发生了异常。

5.4 常用命令行选项让测试更高效

pytest命令有很多有用的选项,这里列举几个最常用的:

  • pytest -v/pytest --verbose: 详细输出,显示每个测试用例的名字和结果。
  • pytest -s: 禁止捕获输出,允许测试中的print语句或日志信息显示在控制台。调试时非常有用。
  • pytest -k \"keyword\": 只运行名称中包含“keyword”的测试函数。例如pytest -k \"add\"会运行所有名字里带“add”的测试。
  • pytest -x: 遇到第一个失败或错误时就停止测试。
  • pytest --maxfail=2: 允许最多失败2个测试,超过则停止。
  • pytest --lf/pytest --last-failed: 只重新运行上一次失败的测试。
  • pytest --tb=short/pytest --tb=no: 当测试失败时,缩短或禁用回溯信息的输出,让报告更简洁。
  • pytest tests/: 指定运行特定目录下的测试。
  • pytest test_file.py::test_function: 运行指定文件中的指定测试函数。

6. 常见问题与排查技巧实录

6.1 问题:运行pytest命令提示“找不到命令”

排查

  1. 确认pytest已安装:pip list | grep pytest
  2. 如果已安装但仍找不到,很可能是Python环境问题。尝试使用python -m pytest来代替pytest命令。这会确保使用当前Python解释器下的pytest模块。
  3. 在PyCharm、VS Code等IDE中,检查项目解释器(Python Interpreter)是否配置正确,是否包含了pytest。

6.2 问题:pytest找不到我的测试文件或测试函数

排查

  1. 检查命名:确保测试文件以test_开头或以_test.py结尾。确保测试函数以test_开头,或测试类以Test开头且方法以test_开头。
  2. 检查当前目录:在终端中运行pytest时,它会在当前目录及其子目录中搜索测试。确保你在正确的目录下运行。你可以通过pytest <目录路径>来指定搜索目录。
  3. 检查__init__.py:如果你的测试文件在某个包目录内,确保该目录下有一个__init__.py文件(即使是空的),这能帮助Python正确识别包结构。

6.3 问题:ImportError: 无法导入被测模块

排查

  1. 检查PYTHONPATH:pytest运行时需要能导入你的源代码模块。最简单的办法是在项目的根目录下运行pytest。对于src/目录结构,你可能需要在根目录创建一个pytest.ini文件并添加:
    # pytest.ini [pytest] pythonpath = src
    或者在测试文件中使用sys.path来添加路径(不推荐,污染全局路径)。
  2. 检查相对导入:在测试文件中,使用绝对导入(from mypackage.mymodule import func)通常比相对导入更可靠。

6.4 问题:Fixture找不到或作用域问题

排查

  1. 作用域conftest.py中定义的fixture对其所在目录及子目录可见。确保conftest.py放在正确的位置。
  2. 命名冲突:如果多个conftest.py定义了同名的fixture,离测试文件更近的(在更深的子目录中)会覆盖上层的。使用pytest --fixtures命令可以查看所有可用的fixture及其定义位置。
  3. 自动使用(autouse):如果你希望一个fixture自动在每个测试中运行,而不需要作为参数声明,可以设置@pytest.fixture(autouse=True)。但要谨慎使用,避免不必要的性能开销和副作用。

6.5 问题:测试通过但生产代码有问题(测试不充分)

避坑技巧

  1. 覆盖边界条件:不要只测“快乐路径”。一定要测试边界值、异常输入、空值(None, “”, [])、极限值等。例如,对于加法函数,除了常规数字,是否应该处理字符串?如果传入None会怎样?这些思考能帮你写出更健壮的代码。
  2. 使用断言描述信息assert语句可以添加一个可选的错误描述信息,这在断言失败时非常有用。
    assert result == expected, f\"调用add({a}, {b}),期望得到{expected},实际得到{result}\"
  3. 定期审查测试代码:把测试代码当成生产代码一样对待。保持其简洁、可读、可维护。删除过时的测试,重构重复的逻辑。

6.6 一个简单的测试排错流程表

现象可能原因快速检查步骤
运行pytest无任何输出当前目录下无符合命名规则的测试文件1.ls查看目录。
2. 检查文件名是否以test_开头或_test.py结尾。
提示ImportErrorPython路径问题,找不到被测模块1. 在项目根目录运行。
2. 检查pytest.ini中的pythonpath设置。
3. 在测试文件顶部打印sys.path查看。
测试函数没被执行函数命名不符合规则1. 确认函数名以test_开头。
2. 确认类名以Test开头(如果函数在类中)。
Fixture未注入Fixture函数名拼写错误,或未定义1. 检查测试函数参数名与fixture函数名是否一致。
2. 运行pytest --fixtures确认fixture是否存在。
断言失败但原因不明断言表达式复杂,或对象比较不直观1. 使用pytest -v -s运行,查看打印的变量值。
2. 在断言中添加自定义错误信息。
3. 使用pdbprint调试。

从创建一个最简单的test_函数,到使用fixture管理资源,再到用参数化覆盖多种场景,pytest的入门曲线其实非常平缓。它的核心思想就是“简单”。不要被网上那些复杂的插件和架构吓到,绝大多数项目的测试需求,用我们今天介绍的这些基础功能就足以优雅地解决。记住,测试的目的是为了给你信心,而不是成为负担。先从为一个简单的函数写一个清晰的测试开始,慢慢养成习惯,你会发现代码的质量和你的开发体验都会得到显著的提升。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/24 11:33:45

Selenium弹框处理实战:5大场景与避坑指南

1. 项目概述&#xff1a;为什么弹框处理是Selenium的“必修课”&#xff1f;如果你用过Selenium做UI自动化&#xff0c;那你一定遇到过弹框。这玩意儿就像是你开车时突然弹出的广告牌&#xff0c;处理不好&#xff0c;整个自动化流程就“撞车”了。我见过太多新手写的脚本&…

作者头像 李华
网站建设 2026/6/24 11:31:43

YOLOv11与超图学习:目标检测工业落地的技术分水岭

1. 这份arxiv论文整理不是“资料包”&#xff0c;而是一张目标检测技术演进的实时快照 你点开这份标题为“arxiv论文整理20260531-0606&#xff08;目标检测方向&#xff09;”的文档时&#xff0c;大概率心里想的是&#xff1a;又一份PDF合集&#xff1f;点开下载、解压、丢进…

作者头像 李华
网站建设 2026/6/24 11:30:37

Selenium自动化测试与数据采集实战:从原理到Page Object模式

1. 项目概述&#xff1a;为什么我们需要Selenium&#xff1f; 如果你是一名测试工程师&#xff0c;或者正在尝试从网页上批量获取数据&#xff0c;那么“浏览器自动化”这个词对你来说一定不陌生。而提到浏览器自动化&#xff0c;Selenium几乎是绕不开的名字。它不是一个简单的…

作者头像 李华
网站建设 2026/6/24 11:28:10

自动驾驶决策算法实战:行为合理性与人机共驾边界

1. 这不是论文&#xff0c;是我在实车调试现场写下的决策算法手记 “自动驾驶中决策算法的思考”——看到这个标题&#xff0c;你可能下意识想点开一篇讲MDP、POMDP或者强化学习公式的长文。但我要先说清楚&#xff1a;这篇不是学术综述&#xff0c;也不是技术白皮书&#xff0…

作者头像 李华
网站建设 2026/6/24 11:22:05

GLM-5.1工程语义理解:国产AI编程的交付能力跃迁

1. 这不是一次普通升级&#xff1a;GLM-5.1为何让整个国产AI编程圈集体“破防” “GLM-5.1上线&#xff0c;Coding Plan瞬间断货”——这不是营销话术&#xff0c;是我亲眼在智谱官网排队时刷新页面看到的实时状态。三分钟内&#xff0c;Lite、Pro、Max三个订阅档位全部显示“售…

作者头像 李华
网站建设 2026/6/24 11:18:23

Pytest自动化测试环境切换实战:配置分离与动态注入方案详解

1. 项目概述&#xff1a;为什么环境切换是自动化测试的“命门” 干了这么多年自动化测试&#xff0c;我越来越觉得&#xff0c;能把测试用例写出来只是入门&#xff0c;真正决定项目成败的往往是那些“不起眼”的工程化细节。环境切换就是其中最典型的一个。想象一下这个场景&a…

作者头像 李华