如果你是跟着某套教程学Python学到了pytest这一章,又正好卡在课后练习上,那这篇内容应该能帮到你。pytest这个东西,光看概念容易飘,真正把它用熟,靠的就是把练习里的题一道道啃下来。我当年学的时候也是这样,看完讲解觉得懂了,一动手写测试就各种报错,后来老老实实把每道课后题做透,才算是真正入了门。这篇就围绕pytest的课后练习,把涉及的核心概念、做题思路、完整的实操过程和常见坑都捋一遍,给正在做练习的你一个参考。
1. 学pytest最先要搞懂的几个核心概念
课后练习里的题看起来花样多,但归根结底都在考几个核心点。先把这些点弄明白,做题的时候你就能看出来每道题到底在考什么,而不是对着题目发呆。
1.1 测试函数怎么组织、断言怎么用
pytest里最基本的单位是测试函数,命名上有硬规矩:函数名必须以test_开头,或者以_test结尾。只有满足这个规则,pytest才会把它当作一个测试用例收集并执行。这个设计不是随便定的,它让pytest在收集测试用例的时候不需要去读函数内部的代码,只要扫一遍名字就能识别出哪些是测试,效率很高,也避免了把普通辅助函数误当成测试。
断言是测试的灵魂。pytest用的是Python原生的assert语句,这跟很多其他语言里专门的断言方法不一样。你只需要写assert 表达式,表达式为真就通过,为假就失败。比如你要测一个加法函数:
def add(a, b): return a + b def test_add(): assert add(2, 3) == 5 assert add(-1, 1) == 0这里第二个断言如果失败了,pytest会告诉你具体的左右值是多少,方便你定位问题。课后练习里最常见的断言类型就这么几种:比较运算结果、判断某个值在不在一个集合里、检查对象属性、验证抛出了什么异常。把这些基本写法练熟,大部分题都能应付。
1.2 fixture到底是干什么用的
fixture是pytest里最值钱的概念,也是课后练习里必然出现的重点。说白了,fixture就是测试前的准备工作。比如你要测试一个操作数据库的函数,每次测试前得先建好测试用的数据,测完还得清理掉。如果每个测试函数里都写一遍这些准备代码,那代码量会爆炸,而且改起来也麻烦。fixture解决的就是这个问题。
定义一个fixture很简单:
import pytest @pytest.fixture def user_data(): return {"name": "test_user", "age": 18}然后在测试函数里,把user_data当作参数传进去,pytest会自动帮你调用这个fixture,把返回值传给测试函数:
def test_user(user_data): assert user_data["name"] == "test_user"fixture的作用域也值得注意。默认每个测试函数执行前都会重新调用一次fixture,这叫函数级。如果某个fixture准备数据的开销比较大,你可以把它设置成模块级或者会话级,让多个测试共用同一个实例。课后练习里经常会让设计一个带scope="module"的fixture,目的就是考察你对作用域的理解。
1.3 参数化为什么是pytest的杀手锏
参数化是我觉得pytest最强大的功能之一,课后练习题里也喜欢考这个。一句话解释:同一个测试逻辑,用多组不同的输入数据去跑。比如你的函数要能被2整除才返回True,正常思路是写好几个测试函数分别测不同的数字,但有了参数化,一个函数就搞定:
import pytest @pytest.mark.parametrize("num, expected", [ (2, True), (3, False), (4, True), (5, False), ]) def test_is_even(num, expected): assert (num % 2 == 0) == expectedpytest会把每组数据当成一个独立的测试用例来跑,如果其中一组失败,其他组照常执行,测试报告里也能清楚看到是哪组数据出了问题。练习题里只要能提取出“同一逻辑、多组数据”的测试场景,用参数化基本就是最优解。有的练习题还会考察参数化跟fixture混用,比如参数里传入某个值,fixture根据这个值准备不同数据,这个组合灵活度很高,值得多练几遍。
2. 课后练习的典型题型解析
弄懂了核心概念,下面说说课后练习题到底长什么样、考查的是什么。我挑几类出现频率高的题型出来拆解。
2.1 基础函数测试练习
这类题给一个写好的函数,让你写测试来验证它的正确性。比如给一个判断字符串是否为回文的函数:
def is_palindrome(s): s = s.replace(" ", "").lower() return s == s[::-1]看起来很简单的题,其实藏着不少考点。空字符串算不算回文?"A"单个字符应该返回什么?带空格和大小写的字符串能不能正确处理?如果只写assert is_palindrome("level") == True就算做完,那你其实只测了一种情况。练习的目的就是让你考虑边界值:空串、单字符、纯符号、大小写混编、带空格的句子。即便pytest本身没有“边界值”的概念,测试思维得有,课后练习考的就是这个。
2.2 异常测试与边界用例
有一类题专门考“函数应该抛异常的情况”。比如一个除法的函数:
def divide(a, b): if b == 0: raise ValueError("除数不能为0") return a / b要验证它确实在b=0时抛出ValueError,可以用pytest.raises:
import pytest def test_divide_by_zero(): with pytest.raises(ValueError): divide(10, 0)这里有几层考察点。第一层是知道pytest.raises这个上下文管理器怎么写。第二层是验证异常信息对不对,可以在pytest.raises里加match参数:pytest.raises(ValueError, match="除数不能为0")。第三层是有些练习题会故意把raise放在不对劲的地方,比如先处理了别的情况,最后才抛异常,你要能判断哪些输入可能触发异常。
这类题的价值在于培养一种意识:测试不光是测“正常运行”,更要测“异常路径”。很多初学者拿到函数就开始写正常用例,结果一小半的异常分支从来没被覆盖过。课后练习里反复出现异常测试,就是在帮你把这个习惯掰正过来。
2.3 fixture复用练习
这类题通常会给你一个类或者一个外部资源,让你写一个fixture来统一管理它的生命周期。常见场景是一个临时的临时文件目录,或者一个模拟的消息队列。比如你有一个函数要把数据写入文件,测试的时候不想污染真实目录,那就设计一个fixture生成临时目录:
import pytest import os @pytest.fixture def temp_dir(tmp_path): target = tmp_path / "data" target.mkdir() return target def test_write_file(temp_dir): file_path = temp_dir / "test.txt" file_path.write_text("hello") assert file_path.read_text() == "hello"这里tmp_path是pytest自带的一个fixture,专门用来提供临时目录,测试结束会自动清理。练习的考点包括:你知道pytest内置了哪些fixture、能不能自定义一个fixture去组合它们、fixture的返回值和测试函数参数之间的对应关系懂不懂。我见过有人把fixture当普通函数来手动调用,结果完全走不通,就是因为没理解“pytest根据参数名自动注入”这个机制。
这道题其实也暗示了fixture的一个重要用法:管理外部资源的生命周期,准备和清理放一起。练习里用临时目录作为例子,实际项目里对应的就是数据库连接、网络请求这些昂贵资源的准备和释放。
3. 实操记录:一个完整的课后练习流程
光讲概念和题型有点虚,我带你把一道典型的课后练习完整做一遍,从读题到跑通,每一步都展示落地操作。
模拟练习:写一个字符串处理的工具模块,包含两个函数——reverse_words(s)把一句话里的单词顺序反转(不是反转字符)、remove_duplicates(lst)去掉列表里相邻的重复元素。要求用pytest写一组测试,覆盖正常情况、边界情况和异常输入,并且使用参数化和fixture。
3.1 准备工作
先把实现写出来,哪怕是练习也要有一个被测对象。我建一个项目目录,里面有string_utils.py存放待测函数,test_string_utils.py存放测试代码。
# string_utils.py def reverse_words(s): if not isinstance(s, str): raise TypeError("输入必须是字符串") return " ".join(s.split()[::-1]) def remove_duplicates(lst): if not isinstance(lst, list): raise TypeError("输入必须是列表") result = [] for item in lst: if not result or item != result[-1]: result.append(item) return result注意reverse_words里我用的是isinstance做类型检查,这样不符合类型时抛出TypeError。这个设计是为后面的异常测试铺路,练习的时候你也要主动去想:我的函数应该在什么条件下抛异常。
3.2 逐步设计测试用例
这一步是我认为最见功夫的地方。我先把能想到的情况列出来,再整理成测试代码。
对于reverse_words,正常情况有:普通英文句子、多个连续空格、空字符串、单个单词。边界情况有:非字符串输入应该抛TypeError。因为正常情况都是“一个输入对应一个输出”,天然适合参数化:
# test_string_utils.py import pytest from string_utils import reverse_words, remove_duplicates @pytest.mark.parametrize("s, expected", [ ("hello world", "world hello"), ("a b c", "c b a"), ("hello world", "world hello"), # 多个空格 ("", ""), # 空字符串 ("single", "single"), # 单个单词 ]) def test_reverse_words_normal(s, expected): assert reverse_words(s) == expected def test_reverse_words_invalid_input(): with pytest.raises(TypeError): reverse_words(123)注意到第二组"hello world",我特意放了三个空格,验证split()确实能处理多个连续空格。这就是测试思维:你不光测“正常”,还要测“有点脏但有意义的输入”。空字符串也是典型的边界值,很多实现会因为split()的结果是空列表而返回空字符串,这里我们就验证了这一点。
remove_duplicates的测试,除了正常情况,我打算用fixture来准备测试数据。因为“相邻重复”和“非相邻重复”的场景不同,我想用一个fixture返回基础列表,再让各个测试基于它构造自己的数据。
@pytest.fixture def sample_list(): return [1, 2, 2, 3, 3, 3, 4, 5, 5] def test_remove_duplicates_normal(sample_list): assert remove_duplicates(sample_list) == [1, 2, 3, 4, 5] def test_remove_duplicates_no_adjacent(): assert remove_duplicates([1, 2, 1, 2]) == [1, 2, 1, 2] def test_remove_duplicates_all_same(): assert remove_duplicates([3, 3, 3]) == [3] def test_remove_duplicates_empty(): assert remove_duplicates([]) == [] def test_remove_duplicates_invalid_input(): with pytest.raises(TypeError): remove_duplicates("abc")这里test_remove_duplicates_no_adjacent是故意设计的非典型情况:列表里有重复的元素,但它们不相邻,所以不应该被去掉。这种用例最容易写错,很多初学者第一反应是把所有重复的都去掉,但这种“相邻去重”的语义如果你不仔细看实现是看不出来的,测试的意义就在这里。
3.3 运行与验证
写完测试以后,在项目目录下运行:
pytest test_string_utils.py -v-v参数让每个测试单占一行显示结果。如果一切顺利,你会看到所有测试都通过。如果有失败,pytest会显示断言失败的具体位置,以及期望值和实际值的对比。
我第一次跑的时候,test_reverse_words_invalid_input这个用例是用reverse_words(None)来测的。结果运行的时候发现isinstance(None, str)是False,所以正确抛出了TypeError。但是换了个思路,用reverse_words(123)又测试了一次,发现也抛了异常,说明类型检查是生效的。这种“换不同输入多次验证”的习惯值得保留。
另外一个小提示:写测试的时候注意命名。我用了test_xxx_normal和test_xxx_invalid_input这样的区分,如果测试失败了,你不用看日志内容也能大致猜到是哪个场景出了问题。课后练习之所以要求你写多个测试,而不是在一个函数里堆一堆assert,就是为了让你学会“一个测试验证一个独立行为”的组织方式。
3.4 关于conftest.py的一个实操经验
做课后练习的时候,你可能会遇到好几个不同文件的测试都要用同一个fixture的情况。如果在每个测试文件里都定义一遍,代码就重复了。正确做法是建一个conftest.py,把公共的fixture放在里面,pytest会自动发现它,同一个目录下的所有测试文件都能直接使用。
刚开始我并不知道这个机制,到处复制fixture定义,后来才知道原来conftest.py就是干这个的。练习题里如果出现多个测试文件,往往就是逼你用conftest.py。你只要把fixture定义放进conftest.py,测试文件里的函数参数名保持跟fixture名一致就行,其他什么都不用改,pytest自动搞定。这一点在课后练习里可能不会直接考,但它是你做综合练习时绕不开的操作。
4. 常见报错与排查心得
做课后练习的时候,跑出报错是家常便饭。我把常见的几类问题和排查思路整理一下,方便你遇到的时候对症下药。
4.1 fixture找不到
报错信息大概长这样:fixture 'xxx' not found。这种问题多半是拼写不一致,或者fixture定义的位置不对。pytest查找fixture的顺序是:测试文件内的定义、当前目录的conftest.py、上级目录的conftest.py、以及插件提供的内置fixture。如果你在test_xxx.py里定义了一个fixture,但在另一个文件里要用它,那就必须先定义一个conftest.py把它放进去。另外检查一下函数名有没有打错,比如定义的是@pytest.fixture的user_daata,测试函数参数写的是user_data,那必然找不到。
注意:fixture查找是按名字匹配的,pytest不会帮你纠正拼写错误。这个报错百分之八十都是拼写问题,仔细核对两边名字,比翻日志更高效。
4.2 断言失败
断言失败是练习里最常遇到的报错,表现形式是红色FAILED,后面跟着一段对比信息。pytest拿Python原生的assert做断言,因此当断言为假时它会把左右两边的值都显示出来。我看到很多初学者一看到FAILED就慌了,其实只需要读一行信息:哪个文件、第几行、期望值是啥、实际值是啥。比如:
E AssertionError: assert 'hello world' == 'world hello'一目了然,函数把句子原样返回了,说明split()或者[::-1]用错了。先读报错里的值,再回头看我函数逻辑,通常很快就能定位。课后练习里有些题会故意留一个小坑,比如把==写成=,这种语法错误pytest也会直接展示出来,改正即可。
4.3 参数化数据格式错误
使用@pytest.mark.parametrize时,如果参数的数量和测试函数的形参数量对不上,pytest会报错。比如:
@pytest.mark.parametrize("a, b, c", [ (1, 2), (3, 4), ]) def test_foo(a, b, c): ...这里每组数据只有两个元素,但函数要三个参数,运行时就报错。解决方式是让每组数据都补齐第三个值,或者把字符串里的参数名调整一下。还有一个常见的坑:字符串里参数名写错了,比如写的是"a, b",但函数里参数叫a, c,pytest会直接告诉你参数不匹配。这类报错其实是在帮你校验参数名和数量,练习中遇到不必烦躁,逐条对齐即可。
我发现一个特别容易踩的坑:在参数化数据里用了中文逗号,,导致pytest把整个字符串当成一个参数名,然后报找不到这个fixture。手动敲的时候一定要检查是不是全角标点,这个问题我至少见过三次。
4.4 测试顺序相关的坑
pytest默认按文件内的定义顺序来收集测试,但不保证每个文件的执行顺序是怎样的。如果你的测试之间有依赖,比如第二个测试希望第一个测试已经往数据库里写了数据,那这个设计本身就有问题。好的测试应该互相独立,不依赖执行顺序。
课后练习里可能不会直接考到这些,但如果你写的测试比较多,就会发现某个测试单独跑能过,全量跑就挂了,多半就是测试之间有间接依赖。排查的方法很简单:用pytest -k 测试名单独跑那个测试,如果能过说明它本身没问题,问题出在和其他测试的交互上。这时候你需要用fixture把初始化数据做干净,而不是靠“执行顺序恰好对了”来维持。
5. 认真做完练习之后我的一些个人体会
pytest这套东西,学得快容易,学得扎实难。课后练习的价值,恰恰不在于让你记住几个API怎么拼,而是逼着你把思路从“写代码”切换到“验证代码”。我做完这些练习之后,最大的感受是自己写函数的时候变得谨慎了,会下意识想:如果别人调我这个函数,传入一个异常值会怎么样?我这个函数的状态变化波及到其他模块了吗?这种意识不是看教程能带来的,是写测试写出来的。
我在写每一个测试之前,都会先问自己一个问题:这个测试到底在验证什么行为?如果答不上来,说明我对被测代码的行为定义还不够清晰,这时我会先回去读实现。很多练习题其实是在训练这种“行为驱动”的思考方式——先明确输入和输出,再写测试去锁住这个行为,最后才是实现细节。
给正在做练习的你一个建议:不要急着把所有题一次性写完再运行。每写完一个测试函数,就跑一下pytest 测试文件名::测试函数名,专注验证当前这一个测试。这样做的好处是,出问题的时候你只需要排查一个用例,信息很干净。等这个用例通过了,再写下一个。我一开始图省事一次写一大坨然后一起跑,结果一堆报错混在一起,定位起来特别费劲。后来改成写完一个跑一个,速度反而快了不少。
另外,自己在练习之外也可以试着模仿题目风格,给身边的功能写几个测试。比如写一个简单的购物车类,或者文件读写工具,然后用pytest把这些功能的正常路径和异常路径都覆盖一遍。练到能不看文档默写出fixture、参数化、raises的基本写法,你pytest就算入门了。到时候回头再看课后练习,你会发现那些题其实都在反复打磨同一套功夫:写清晰的测试函数、用合理的组织方式、做充分的场景覆盖。把这些练扎实,比死记硬背二十个API有用得多。