小唐那天接了个活,给一个老项目补测试。
项目里已经有一些用unittest写的测试,长这样:
import unittest
class TestCalculator(unittest.TestCase):
def test_add(self):
self.assertEqual(add(1, 2), 3)
小唐习惯用pytest,觉得assert写起来更短,于是他在同一个目录下新建了一个测试文件,写了个fixture,准备在测试里注入一些测试数据:
import pytest
import unittest
@pytest.fixture
def sample_data():
return {"a": 1, "b": 2}
class TestProcessor(unittest.TestCase):
def test_process(self, sample_data):
result = process(sample_data)
self.assertEqual(result, 3)
跑pytest,测试被收集到了,但直接报错:
TypeError: test_process() missing 1 required positional argument: 'sample_data'
小唐愣了:“sample_data明明是个fixture,pytest为什么不把它注入进来?”
他试着把类改成普通类,不继承unittest.TestCase:
class TestProcessor:
def test_process(self, sample_data):
result = process(sample_data)
assert result == 3
这次fixture正常注入了,测试通过。但他又发现,文件里原来那些继承unittest.TestCase的测试,assertEqual还能用,而新写的普通类里self.assertEqual直接报AttributeError。
小唐陷入了困惑:同一个测试文件里,两种写法混在一起,行为完全不一样。pytest到底是怎么看待unittest.TestCase的?
这个问题,就是pytest和unittest关系里最容易让人栽跟头的地方。
先看一个最直观的对比
同一个测试,用两种框架写:
# unittest 风格
import unittest
class TestMath(unittest.TestCase):
def setUp(self):
self.data = [1, 2, 3]
def test_sum(self):
self.assertEqual(sum(self.data), 6)
def test_len(self):
self.assertEqual(len(self.data), 3)
# pytest 风格
import pytest
@pytest.fixture
def data():
return [1, 2, 3]
def test_sum(data):
assert sum(data) == 6
def test_len(data):
assert len(data) == 3
功能一样,但写法完全不同。
unittest是基于类的:测试必须写在继承unittest.TestCase的类里,方法名以test_开头,断言用self.assertEqual、self.assertTrue这些方法,初始化和清理用setUp、tearDown。
pytest是基于函数的:测试就是普通函数,函数名以test_开头,断言直接用assert,初始化和清理用fixture,通过函数参数注入。
用一个生活场景来理解。
unittest像一套正式的公文模板:你要填表,表头、表格、签章位置都是固定的,你必须按格式来。
pytest像便利贴:你想写什么就写什么,只要在开头写个标题,剩下的随你发挥。
区别一目了然:一个要求你遵守框架的规矩,一个尽量不干扰你写代码。
核心区别一:测试收集机制不同
unittest的测试收集依赖类继承。只有继承unittest.TestCase的类,里面的test_*方法才会被当成测试。unittest自己的TestLoader会扫描模块,找到这些类,实例化它们,然后逐个运行测试方法。
pytest的收集规则更宽:它扫描文件,找到所有以test_开头的函数,以及所有以Test开头的类(不要求继承任何基类),把里面的test_*方法当成测试。函数和类都可以。
这个差异直接导致了一个结果:**pytest能运行unittest写的测试**。因为pytest的收集器会识别unittest.TestCase的子类,把它们当成测试类来处理。所以你可以用pytest命令跑一个纯unittest的项目,测试能正常执行。
但反过来不成立:unittest不能直接运行pytest风格的函数测试,因为unittest只认TestCase子类。
这就是小唐遇到的第一个问题的根源:pytest能收集到TestProcessor,因为它是unittest.TestCase的子类。但pytest在运行这个类的测试方法时,不会走自己的fixture注入机制,因为unittest.TestCase的测试方法签名是固定的(self),pytest不会往里面塞额外的参数。所以sample_data参数没有被注入,调用时自然报“缺少参数”。
核心区别二:fixture 和 setUp/tearDown
unittest的初始化用setUp,清理用tearDown。每个测试方法运行前都会调用一次setUp,运行后调用一次tearDown。类级别的用setUpClass和tearDownClass。
class TestDB(unittest.TestCase):
def setUp(self):
self.conn = create_connection()
def tearDown(self):
self.conn.close()
def test_query(self):
result = self.conn.execute("SELECT 1")
self.assertIsNotNone(result)
pytest的fixture用@pytest.fixture装饰器定义,通过函数参数注入。fixture可以有作用域(function、class、module、session),可以依赖其他fixture,可以在yield之后做清理。
@pytest.fixture
def conn():
c = create_connection()
yield c
c.close()
def test_query(conn):
result = conn.execute("SELECT 1")
assert result is not None
两者的能力差距在于组合性。unittest的setUp和测试方法绑定在同一个类里,一个类只能有一套setUp。如果两个测试需要不同的初始化逻辑,你要么写两个类,要么在setUp里加条件判断。
pytest的fixture可以按需组合。一个测试可以同时请求多个fixture,fixture之间可以互相依赖,fixture还可以参数化。这种组合能力在处理“数据库连接 + 临时文件 + mock服务”这种多资源场景时优势非常明显。
但要注意:fixture不会注入到unittest.TestCase的子类里。这是小唐踩的坑,也是混用两种风格时最容易出错的地方。如果你要用fixture,测试类就不要继承unittest.TestCase。
核心区别三:断言风格
unittest提供了一整套断言方法:assertEqual、assertTrue、assertIn、assertRaises、assertAlmostEqual等等。每种比较都有对应的方法,失败时的报错信息也比较详细。
self.assertEqual(a, b)
self.assertTrue(condition)
self.assertIn(item, container)
self.assertRaises(ValueError, func)
pytest直接用Python的assert语句,配合pytest的断言重写机制,失败时能给出详细的对比信息。
assert a == b
assert condition
assert item in container
with pytest.raises(ValueError):
func()
pytest的assert在失败时,会自动展示表达式中各个部分的值。比如assert a == b失败,它会打印a和b的具体值,不需要你手动写错误信息。这是pytest的断言重写(assertion rewriting)在起作用:它在导入测试模块时,把assert语句改写成带详细输出的版本。
unittest的断言方法在失败时也有信息,但通常不如pytest的assert直观。而且unittest的断言方法必须通过self调用,这意味着测试方法必须在TestCase子类里,这也限制了测试的组织方式。
核心区别四:参数化
unittest没有内置的参数化机制。想用不同的输入跑同一个测试,要么写多个测试方法,要么用subTest:
class TestAdd(unittest.TestCase):
def test_add(self):
cases = [(1, 2, 3), (0, 0, 0), (-1, 1, 0)]
for a, b, expected in cases:
with self.subTest(a=a, b=b):
self.assertEqual(add(a, b), expected)
subTest能让一个测试方法里跑多组数据,某组失败不影响其他组,但写法比较啰嗦,而且失败时的报告不如独立的测试用例清晰。
pytest用@pytest.mark.parametrize装饰器,一行搞定:
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
每组数据都会被当成一个独立的测试用例,失败时能精确指出是哪一组数据失败。参数化还可以叠加,多个parametrize装饰器会生成笛卡尔积。
这个差异在数据驱动的测试里非常明显。unittest的subTest能用,但pytest的parametrize更简洁,报告更清晰,组合能力更强。
核心区别五:插件生态
unittest是标准库的一部分,插件生态基本为零。它的扩展主要靠继承TestCase和自定义TestLoader,门槛高,实践少。
pytest的插件生态非常庞大:pytest-cov做覆盖率,pytest-xdist做并行,pytest-mock做mock,pytest-asyncio做异步测试,pytest-html生成HTML报告,pytest-rerunfailures做失败重试。这些插件通过pytest的hook机制接入,安装后直接生效,不需要改测试代码。
这是pytest在实际项目里被广泛采用的重要原因之一。测试框架不只是“运行测试”,还包括报告、覆盖率、并行、mock、异步支持等等。pytest把这些交给了插件生态,unittest则基本只提供核心功能。
坑一:在 unittest.TestCase 里用 fixture
这就是小唐踩的坑。
class TestProcessor(unittest.TestCase):
def test_process(self, sample_data): # fixture 不会注入
...
pytest能收集这个类,但不会给unittest.TestCase的方法注入fixture。sample_data会被当成一个普通参数,调用时缺少实参,直接报TypeError。
解决方案有两个:要么把类改成普通类(不继承unittest.TestCase),用assert代替self.assertEqual;要么放弃fixture,改用setUp。
不要在一个测试类里混用两种风格。 继承unittest.TestCase就用unittest的方式;要用pytest的fixture和assert,就用普通类和函数。
坑二:以为 pytest 运行 unittest 测试时行为完全一致
pytest能运行unittest.TestCase的测试,但不是所有行为都一致。
比如pytest的-k过滤、-x快速失败、--pdb调试,对unittest.TestCase的测试基本可用。但pytest的一些高级特性,比如fixture注入、parametrize装饰器直接作用在unittest.TestCase的方法上,是不生效的。
parametrize作用在unittest.TestCase的方法上时,pytest会报错或者忽略参数化。因为unittest.TestCase的方法签名是固定的(self),pytest无法把参数传进去。
所以“pytest能跑unittest测试”这句话,准确的理解是:**pytest能执行unittest的测试,但不会给它们加上pytest特有的能力**。混用时,unittest的测试还是按unittest的规则运行,pytest的测试按pytest的规则运行,两套规则互不干扰,但也互不增强。
坑三:断言方法在普通类里不存在
反过来,如果你把测试类改成普通类,却还在用self.assertEqual,会报AttributeError:
class TestProcessor: # 不继承 unittest.TestCase
def test_process(self):
self.assertEqual(1, 1) # AttributeError
因为assertEqual是unittest.TestCase的方法,普通类没有。普通类里要用pytest的assert:
class TestProcessor:
def test_process(self):
assert 1 == 1
这个坑在迁移测试时特别常见:把类继承去掉了,断言方法忘了改。
坑四:setUp 和 fixture 的执行顺序混乱
如果一个测试文件里既有unittest.TestCase的类,又有pytest风格的函数测试,两者的初始化逻辑是各自独立的。unittest的setUp不会影响pytest的函数测试,pytest的fixture也不会注入到unittest的类里。
这本身不是bug,但如果你以为“在setUp里准备了数据,所有测试都能用”,就会出问题。两套机制各管各的,数据不共享。
解决方案是统一风格。要么全用unittest,要么全用pytest。如果必须混用(比如老项目逐步迁移),要清楚两套机制的边界,不要指望它们互相打通。
什么时候用 unittest,什么时候用 pytest?
判断标准:
项目已经在用unittest,测试量很大,迁移成本高:继续用unittest,没必要为了“新”而重写。pytest能运行unittest测试,你可以在新测试里用pytest,老测试保持不动。
新项目,没有历史包袱:用pytest。fixture、parametrize、assert、插件生态,这些优势在项目变大后会越来越明显。
环境受限,不能安装第三方包:用unittest。它是标准库的一部分,零依赖。
需要和Django、Flask等框架的测试工具集成:两者都支持,但pytest通常有更成熟的插件(pytest-django、pytest-flask)。
**团队不熟悉pytest,但熟悉unittest**:先用unittest,降低学习成本。pytest的学习曲线不陡,但fixture的作用域和依赖注入需要一点时间理解。
回到小唐的故事
小唐最终的方案是:老测试不动,新测试用pytest风格。
他把新写的TestProcessor改成了普通类,用assert代替self.assertEqual,fixture正常注入。老的unittest.TestCase测试保持原样,pytest照样能跑。
# 老测试,保持 unittest 风格
class TestCalculator(unittest.TestCase):
def test_add(self):
self.assertEqual(add(1, 2), 3)
# 新测试,用 pytest 风格
class TestProcessor:
def test_process(self, sample_data):
assert process(sample_data) == 3
他在文档里写了一条:“pytest能跑unittest测试,但不会给unittest.TestCase注入fixture,也不会给它的方法加parametrize。混用时,两套规则各管各的。新测试用pytest风格,老测试保持unittest风格,不要在一个类里混两种写法。”
pytest和unittest的区别,本质上是“约定式框架”和“配置式框架”的区别。unittest要求你按它的模板来,好处是规则明确;pytest尽量不干扰你写代码,好处是灵活,但需要你理解它的收集规则和fixture机制。
选哪个,取决于你的项目现状、团队习惯、以及对插件生态的需求。两者不是替代关系,而是可以共存的关系。关键是不要在同一个测试类里混用两种风格,否则就会出现小唐那种“fixture不注入、断言方法不存在”的困惑。