资讯动态

pytest课后练习复盘:从断言到fixture,夯实测试基本功

发布时间:2026/10/15 13:12:43 来源:尧图企业网站定制
最近跟着一套Python进阶视频里的pytest章节把课后练习从头到尾完整刷了一遍。说实话看视频的时候感觉自己什么都懂了断言、fixture、参数化、跳过每个概念都听明白了可真到动手写那些课后练习的时候一下子就懵了断言到底怎么写才规范fixture怎么在不同文件之间共享参数化的数据要放在哪里这些问题在视频里都有讲但落到自己代码里就各种报错。这套课后练习题量不大一共十来道但每一道都踩在学习pytest的关键点上。做完之后我对pytest的理解完全上了一个台阶再回去看自己之前写的测试代码满眼都是可以改进的地方。这篇内容就当作我对这套课后练习的一次完整复盘把每类题目的考察点、解题思路、参考代码还有做练习时实打实踩过的坑都记下来。适合正在学pytest、尤其是刚看完教程准备动手做练习的人当然如果你已经工作了一段时间想回头梳理自己的测试基本功这里面的一些细节也值得翻一翻。1. 先说说这套课后练习的设计思路1.1 题目背后的知识点地图我在做练习之前先做了一件事把整个章节的知识点列了一遍然后对照课后练习一道一道圈出考察目标。后来发现这个习惯非常有用因为练习题不会告诉你它在考什么你得自己识别识别错了方向代码写得再漂亮也是白搭。这套练习的设计大致覆盖了pytest的这几个核心模块基础断言、fixture、参数化、异常测试、跳过和分组。整个考察顺序也是循序渐进的一开始是单函数测试中间是带状态的fixture最后是参数化和多文件结构。把这张知识点地图画出来后练习就不再是一道道孤立的题而是一个完整的知识体系。我整理了一个简单的对照表方便后来者参考练习类型考察核心难度基础函数测试assert断言、测试函数命名低带初始化/清理的场景fixture、yield、作用域中多组数据测试parametrize参数化中异常路径测试pytest.raises、异常断言中跳过与条件测试skip、skipif、自定义标记中跨文件共享conftest.py、共享fixture中高这张表看起来简单但它能帮你快速定位自己卡在哪里。比如我第一遍做题的时候卡在最久的是fixture的作用域问题而不是断言本身。如果没有这张表我大概会在参数化那题上反复折腾然后误以为是参数写错了实际上是对fixture的scope理解不透。1.2 为什么练习题比纯看视频更锻炼人很多人学习的时候喜欢一直看视频觉得听懂了就是学会了但代码这个东西手感和语感一样必须靠练。课后练习的存在意义就是逼着你把我好像懂了变成我确实写出来了。这套练习特别好的一个地方是它不提供答案文件至少我做的版本里没有。你必须自己想、自己写、自己跑。跑通了还不算完还得故意犯错来验证自己的理解。比如我写完一个测试会故意把断言里的期望值改错然后观察报错信息长什么样。这个方法看似笨但对理解pytest的断言机制帮助极大。练习里还有一类题目让你计算预期输出这其实是训练你对pytest收集规则和退出码的直觉。因为这些细节纯看视频非常容易忽略只有实际跑命令、看输出、数用例数才能真正记住。1.3 这套练习最适合什么人做如果你问我这套课后练习适合谁我的回答是适合所有学pytest但还没有写过完整测试项目的人。不管你是刚接触自动化测试的学生还是写了两年代码但测试经验为零的开发这套练习都能用较低的成本让你进入状态。不过有一点要提醒它不适合完全零基础的人。起码要先懂Python函数、类、模块导入这些基本语法还得知道命令行怎么跑脚本。我在练习中就遇到了因为对Python的模块路径不熟导致conftest.py一直没有生效的情况这个后面详细说。2. 核心用法复盘做题前必须搞懂的三件事2.1 assert断言别只学会等于pytest和unittest相比最直观的优势就是断言。unittest里你得记一大堆assertEqual、assertTrue、assertIn而在pytest里就是Python原生的assert语句。我在练习里第一道题就是给一个加法函数写测试我一开始自然写的是 assert add(2, 3) 5这个没错但后面有一题涉及到对字符串的断言我一开始也用 结果发现一个细节pytest对 失败的输出会把两侧的具体值都打印出来非常方便排查但如果你把 assert 单纯理解为判断等于那你就忽略了 assert 本质是表达式求值。pytest在运行时会把assert后面的表达式拆解成可读的失败信息哪怕你写的是 assert user.name admin and user.is_active它也能告诉你哪个子条件不成立。在做练习时我建议你不要只满足于断言结果是对的还要故意构造失败场景看看提示信息能否让你一眼定位问题。如果失败信息含糊不清说明你的断言写法有问题应该拆分成更细的断言。我踩过的一个小坑是在断言一个浮点数结果时直接写 assert result 0.3结果用例失败。因为浮点数在计算机里是二进制近似存储的0.1 0.2 不等于 0.3。练习里虽然没直接考这个但这是一个很经典的细节最好提前知道否则遇到类似场景会一脸懵。处理方式是使用 pytest.approx 或者 round 之后再比较。2.2 fixture把准备和收尾工作交出去fixture是这套课后练习的重头戏因为它是pytest区别于普通函数调用的灵魂所在。练习里有一类题要求你在测试一个用户登录函数时先初始化一个临时用户测试结束后再清理掉。在没有fixture之前你的第一反应可能是写一个setup函数然后在每个测试函数里调用它。这种写法能用但问题在于第一如果测试函数很多你会在每一个函数里重复调用setup第二如果初始化过程中出错你会很难判断错误究竟来自被测函数还是初始化代码第三测试结束后你忘了清理临时数据就会越积越多。而pytest的fixture机制就是来解决这个问题的你把初始化和清理逻辑写在一个用 pytest.fixture 装饰的函数里把要共享的对象作为参数传给测试函数pytest会自动帮你注入。这里我强烈建议你练熟 yield 的用法。传统的fixture函数用return返回准备的数据但如果有清理动作你需要把清理逻辑写在 yield 之后。也就是说fixture函数在执行到yield时把资源交给测试函数测试函数跑完后控制权会回到fixture继续执行yield后面的清理代码。我当时做练习的时候写的第一版fixture是这样的pytest.fixture def user(): u create_user(test_user) return u测试跑完了数据一直残留后来改成pytest.fixture def user(): u create_user(test_user) yield u delete_user(u.id)问题立刻解决。这个过程其实就对应练习题里数据库残留数据导致第二次跑用例失败的场景。现在你可以再回想一下为什么很多测试教程里强调fixture代替setUp和tearDown原因就在这里fixture把准备、使用、清理放在一个函数内结构清晰而且依赖关系可组合比分散的setUp/tearDown好维护得多。2.3 parametrize参数化一组数据跑一个用例参数化大概是这套练习里最能提效率的一个特性。我做的练习里有一道题要求测试一个判断闰年的函数输入年份不同期望结果也不同。如果不用参数化你得写五个独立的测试函数或者写一个for循环但for循环里一旦有断言失败整个测试函数就停了而且你不知道是哪组数据出的问题。pytest的解法是 parametrize 装饰器。你在装饰器里传两个关键信息参数名和数据列表。数据列表里的每一个元素可以是一个普通值表示只给一个参数也可以是一个元组或者字典表示一次给多个参数。我以闰年函数为例import pytest def is_leap(year): return year % 4 0 and (year % 100 ! 0 or year % 400 0) pytest.mark.parametrize(year,expected, [ (2000, True), (1900, False), (2004, True), (2001, False), (2024, True), ]) def test_is_leap(year, expected): assert is_leap(year) expected这样跑起来pytest会把用例自动展开成5个每一个都有独立的编号即使其中一个失败其他用例依然会继续执行失败信息里也会清楚标注是哪一组数据。练习里还问了一个很细节的问题参数化数据里如果有浮点数或者有需要特殊构造的对象怎么办其实很简单数据列表里的元素可以是任意Python对象你可以在外面先写好对象再放进列表甚至可以用工厂函数。这里的核心心得是参数化不是把循环搬到装饰器里那么简单它把测试数据从测试逻辑中分离出来。将来你的测试数据如果从Excel、JSON、数据库里读取你只需要改数据源逻辑几乎不用动。3. 几道典型课后练习的完整解题过程3.1 练习一给一个计算器模块写基础测试这道题是所有练习的第一题难度不大但暗坑不少。题目大意是有一个计算器模块里面有 add、subtract、multiply、divide 四个函数请为它们编写测试。很多人觉得这题太简单直接写四个函数用 断言完事。但我做的时候特意思考了一下 divide 的边界情况。先看基本代码# calculator.py def add(a, b): return a b def subtract(a, b): return a - b def multiply(a, b): return a * b def divide(a, b): if b 0: raise ValueError(除数不能为0) return a / b对应的测试文件# test_calculator.py import pytest from calculator import add, subtract, multiply, divide def test_add(): assert add(2, 3) 5 assert add(-1, 1) 0 def test_subtract(): assert subtract(5, 3) 2 def test_multiply(): assert multiply(4, 5) 20 def test_divide(): assert divide(10, 2) 5.0 def test_divide_by_zero(): with pytest.raises(ValueError): divide(10, 0)这题看似简单但把异常场景一起覆盖掉才是完整的测试思路。很多初学者会漏掉 test_divide_by_zero觉得正常情况测完就算完成了。实际上没有异常路径测试的用例集在真实项目里根本不敢跑。做这题的时候我还发现了 assert 的一个细节如果你在同一个测试函数里写多个断言一旦加了新断言之前的断言仍然会执行代码是顺序执行的。但多个断言之间其实有关联性如果第一个就失败后面的不会执行。所以我会刻意把互不相关的断言拆到不同测试函数里让失败信息更独立。这道题没有标准答案但一个测试函数聚焦一个行为是我刷完整套练习后最深的体会。3.2 练习二用户登录场景的fixture封装这道题开始有难度了。题目模拟一个用户登录模块要求你在测试前先创建用户测试后清理用户。如果只是简单写一个fixture返回user对象前面也说了会有数据残留问题。因此这道题真正的考点是 fixture 的 yield 清理机制以及 fixture 在多个测试函数之间的复用。我当时写的代码是这样的# test_login.py import pytest class User: def __init__(self, name): self.name name self.is_logged_in False def create_user(name): print(f创建用户{name}) return User(name) def delete_user(user): print(f删除用户{user.name}) def login(user, password): if password secret: user.is_logged_in True return True return False pytest.fixture def prepared_user(): u create_user(test_login_user) yield u delete_user(u) def test_login_success(prepared_user): assert login(prepared_user, secret) is True assert prepared_user.is_logged_in is True def test_login_wrong_password(prepared_user): assert login(prepared_user, wrong) is False assert prepared_user.is_logged_in is False跑一下你会看到控制台先打印创建用户测试函数执行完后再打印删除用户。这就是fixture的生命周期。两个测试函数各自独立每次都会创建和删除用户不会互相污染数据。到这里还没完练习里有一个进阶提问如果两个测试函数希望共用同一个用户对象而不是每次重新创建怎么办这个就涉及 fixture 的 scope 参数。把 pytest.fixture 改成 pytest.fixture(scopemodule)那么同一个测试文件里的多个测试函数会共用同一个用户对象。我一开始以为scope指定成session就是全局的肯定更好后来发现session级的fixture在测试文件很多时会让用例之间产生依赖反而更难排查。所以练习里这个进阶问题其实是在提醒你scope不是越大越好而是要根据测试数据的隔离需求来定。我自己的经验是默认scope是function也就是每个测试函数各自独立这是最安全的选择。只有当你明确知道这个资源在多个测试函数之间共享不会产生副作用时才去提高scope。练习题里如果没明确说明我建议都用默认的function级。3.3 练习三用参数化覆盖边界条件和异常这道题在闰年函数基础上扩展要求用参数化覆盖边界年份包括负数、0、整百年份等。题目也不难但它想锻炼的是你组织测试数据的能力。我写的参考代码如下import pytest def is_leap(year): if year 0: raise ValueError(年份必须为正数) return year % 4 0 and (year % 100 ! 0 or year % 400 0) pytest.mark.parametrize(year, [2000, 2024, 2400]) def test_is_leap_true(year): assert is_leap(year) is True pytest.mark.parametrize(year, [1900, 2100, 2001]) def test_is_leap_false(year): assert is_leap(year) is False pytest.mark.parametrize(year,expected, [ (0, ValueError), (-100, ValueError), ]) def test_is_leap_invalid(year, expected): with pytest.raises(expected): is_leap(year)这种写法把正例、反例、异常例分成三个测试函数每组数据独立展开格式清晰跑起来也方便定位。这里的核心收获是测试数据本身也是一种代码资产要让它读起来像一张清晰的表格而不是一堆散落的魔法数字。另一个细节是关于参数化中的异常测试。我这里把异常类型作为参数传给 pytest.raises这也是一种常用做法。如果你希望让参数化数据更丰富可以用字典或者pytest.param给某个用例单独打标记比如pytest.mark.parametrize(year,expected, [ pytest.param(0, ValueError, idyear_zero), pytest.param(-100, ValueError, idyear_negative), ])这样做的好处是用例失败时报错信息会显示 year_zero 或 year_negative一眼就知道是哪组边界数据出了问题。这个技巧在练习里没直接说但我在做后面一道更复杂的参数化题时频繁用到推荐你在练习阶段就养成习惯。4. 做练习时最容易踩的坑和排查技巧4.1 为什么明明写了测试却一个用例都没跑这个坑我愿称之为新手第一坑。写完测试文件在命令行执行 pytest结果输出显示 collected 0 items。我当时第一反应是代码有问题疯狂检查测试函数有没有语法错误后来才发现是文件命名不符合pytest的默认收集规则。pytest默认收集规则是测试文件必须以 test_ 开头或者test.py 结尾不同版本略有差异测试函数必须以 test开头测试类必须以 Test 开头且不能有init方法。如果命名不对pytest直接不认。我复盘了一下很多教程里用 Jupyter Notebook 演示pytest在notebook里跑可能没问题但一旦你把他给的代码保存成文件就必须严格按照命名规则来。我在练习中就把一个测试文件命名成了 my_tests.py结果跑了个寂寞。快速自检方法也很简单在命令行执行 pytest --collect-only它会清晰地列出pytest收集到了哪些用例。如果你刚写完测试发现实际用例数和预期不符先跑这个命令看看收集情况。4.2 断言失败时看不懂输出怎么办pytest的断言失败输出默认会展示失败处的表达式、具体值和对差值。一开始我觉得这个输出乱七八糟的后来发现其实很有用。比如下面这个失败信息 assert user.is_admin is True E assert False is True它告诉你 user.is_admin 这个表达式求值结果是False期望是True。即使你没有打印user对象也能猜出问题在权限初始化上。但如果你发现失败信息不够直观我建议你做两件事第一在断言失败时pytest会通过重写断言字节码来增强输出但如果你用了比较复杂的表达式尤其是and/or组合失败信息可能不够精确。所以最好是拆断言。宁可多写几个断言也不要在一个断言里塞太多条件。第二可以用 -v 参数让输出更详细每条用例的结果都会显示出来。如果用例很多还可以加上 --tbshort 让错误信息更简洁或者 --tbline 只显示一行错误信息。练习过程中我用的最多的组合是 pytest -v --tbshort信息量合适不会被刷屏。4.3 命令行的灵活用法-v、-k、-m课后练习做到后面会有一个很实际的需求我只想跑某一个测试或者只想跑某类测试怎么办这个在命令行层面就能解决非常有实用价值。-k 参数允许你用关键词过滤用例比如 pytest -k login 表示只运行名称中包含login的测试函数。练习里如果有很多用例你想只看登录相关的那几个就可以用 -k login and not slow 这种语法相当于在命令行做了一次轻量过滤。-m 参数则是配合自定义标记使用的。pytest.mark.slow、pytest.mark.smoke 都是自定义标记你要先在代码里给某些测试函数打上装饰器然后用 pytest -m smoke 只运行冒烟测试。第一次用的时候记得在配置文件里注册标记避免出现PytestUnknownMarkWarning警告。我特别建议你在做完练习后把这些命令行参数全部试一遍。因为真实项目里的测试文件动辄上百个如果只会执行裸露的pytest命令工作效率会非常低。学会用 -k 和 -m 高效选择用例是业余练习和真实工作之间很重要的一道门槛。5. 从练习走向实战conftest和常用插件5.1 conftest.py共享fixture的正确姿势大多数练习做完之后你还没涉及多文件结构但真实项目里fixture经常需要在多个测试文件之间共享。这时候就要用到 conftest.py。这个文件的作用是让pytest自动发现并加载其中的fixture而不用在每个测试文件里显式import。我当时做练习的时候犯过一个错把conftest.py写成了一个普通工具模块里面放各种函数然后在测试文件里自己import。结果发现pytest有自己的一套加载机制你手动import反而可能破坏它的fixture作用域。正确做法是conftest.py里只放fixture和钩子函数不随便写业务逻辑让pytest自己去加载它。我在练习题里模拟过一个场景两个测试文件 test_a.py 和 test_b.py 都需要一个临时目录fixture。我把这个fixture放在conftest.py中两个测试文件都能直接用代码零重复。这个体会真的很深共享fixture不是在每个文件里都写一遍也不是搞一个base类继承而是利用pytest原生的conftest机制按目录层级自动加载。conftest.py还有个特性是分层生效根目录的conftest.py对全项目生效子目录里的conftest.py只对当前目录和它下面的子目录生效。做练习的时候可以先从单层conftest开始然后试着在子目录再放一个内容不同的同名fixture观察优先级和覆盖关系这个过程能让你对fixture查找规则有非常直观的理解。5.2 用pytest-cov检查你的测试覆盖率练习做到最后你可能会好奇我这些测试到底覆盖了被测代码的多少这时候就需要用到覆盖率工具。pytest-cov 是一个第三方插件安装之后可以直接在pytest命令里加 --cov 参数。例如你在项目根目录执行pytest --covcalculator test_calculator.py输出会给出calculator.py里每一行的覆盖情况以及总覆盖率百分比。课后练习里没有强制要求覆盖率但我个人强烈建议你养成这个习惯。因为测试跑通了和测试覆盖到位是两回事。我当时写完第一版测试覆盖率只有62%很多分支没跑到后来根据覆盖率报告补了几个边界用例覆盖率提升到了95%。这个过程是我做题收获最大的一环。覆盖率也不是越高越好尤其是刚入门时为了追求100%可能会写出很多低质量用例。但把它作为练习题的一个辅助视角确实能逼你思考哪些行、哪些分支还没有测到。真实项目里核心模块通常会要求至少80%到90%的行覆盖率但具体标准看团队约定不必盲目追求。5.3 关于这套练习的几点后续建议如果你刷完了所有练习想趁热打铁巩固我个人的后续建议是把练习里的被测函数扩展成一个小项目比如用户管理模块然后你自己设计测试目录结构放上conftest.py用好参数化、fixture、标记、覆盖率甚至接入pytest-html生成一份好看的测试报告。我做完这套练习之后就是用这个方式把知识点串起来的效果比反复看视频好很多。后来在实际工作里我写测试的时候会特别留意fixture的粒度不要一个fixture干太多事参数化的数据也尽量从文件中读取保持测试代码稳定。这些好习惯很大程度上都是在做这套课后练习时打下的底子。它虽然只是一套入门向的练习但如果你认真把它当作一个小工程来做而不是随手写几个函数交差它教给你的东西会远超练习本身的题目范围。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑