以下分别给出unittest和pytest的详细案例,包含常见测试场景:基本断言、异常测试、夹具、模拟、参数化、跳过/预期失败。
1. 被测试代码 (待测模块calculator.py)
# calculator.pyimportrequestsclassCalculator:defadd(self,a,b):returna+bdefdivide(self,a,b):ifb==0:raiseValueError("除数不能为零")returna/bdeffetch_data(url):"""模拟网络请求,返回状态码和JSON"""resp=requests.get(url)returnresp.status_code,resp.json()2. unittest 详细案例
# test_unittest_demo.pyimportunittestfromunittest.mockimportpatch,MockfromcalculatorimportCalculator,fetch_dataclassTestCalculator(unittest.TestCase):"""基本测试 + 夹具 + 模拟 + 参数化 + 跳过"""@classmethoddefsetUpClass(cls):"""所有测试运行前执行一次"""print("\n[setUpClass] 初始化计算器实例")cls.calc=Calculator()@classmethoddeftearDownClass(cls):"""所有测试运行后执行一次"""print("[tearDownClass] 清理资源")defsetUp(self):"""每个测试方法前执行"""print(f"\n[setUp] 准备测试:{self._testMethodName}")deftearDown(self):"""每个测试方法后执行"""print(f"[tearDown] 完成测试:{self._testMethodName}")# ---------- 基本断言 ----------deftest_add_positive(self):self.assertEqual(self.calc.add(2,3),5)self.assertAlmostEqual(self.calc.add(0.1,0.2),0.3,places=6)deftest_divide_normal(self):self.assertEqual(self.calc.divide(10,2),5.0)# ---------- 异常测试 ----------deftest_divide_by_zero(self):withself.assertRaises(ValueError)asctx:self.calc.divide(5,0)self.assertEqual(str(ctx.exception),"除数不能为零")# ---------- 模拟 (mock) ----------@patch('calculator.requests.get')deftest_fetch_data_success(self,mock_get):# 配置模拟对象mock_response=Mock()mock_response.status_code=200mock_response.json.return_value={"key":"value"}mock_get.return_value=mock_response status,data=fetch_data("http://example.com")self.assertEqual(status,200)self.assertEqual(data,{"key":"value"})mock_get.assert_called_once_with("http://example.com")@patch('calculator.requests.get')deftest_fetch_data_failure(self,mock_get):mock_response=Mock()mock_response.status_code=404mock_response.json.side_effect=Exception("Not Found")mock_get.return_value=mock_responsewithself.assertRaises(Exception):fetch_data("http://example.com/bad")# ---------- 参数化测试 (使用 subTest) ----------deftest_add_multiple_inputs(self):test_cases=[(1,2,3),(-1,-1,-2),(0,0,0),(100,-50,50)]fora,b,expectedintest_cases:withself.subTest(a=a,b=b,expected=expected):self.assertEqual(self.calc.add(a,b),expected)# ---------- 跳过测试 ----------@unittest.skip("演示跳过,该功能尚未实现")deftest_multiply_not_implemented(self):pass@unittest.skipIf(True,"条件满足时跳过")deftest_skip_conditionally(self):pass@unittest.expectedFailuredeftest_expected_failure(self):# 这个断言会失败,但不会计入失败数self.assertEqual(1,2)if__name__=='__main__':unittest.main(verbosity=2)运行方式:python test_unittest_demo.py或python -m unittest test_unittest_demo
3. pytest 详细案例
# test_pytest_demo.pyimportpytestfromunittest.mockimportMock,patchfromcalculatorimportCalculator,fetch_data# ---------- 夹具 (fixture) ----------@pytest.fixture(scope="class")defcalculator():"""类级别夹具,返回计算器实例"""print("\n[fixture] 创建计算器实例")calc=Calculator()yieldcalcprint("[fixture] 销毁计算器实例")@pytest.fixturedefsample_data():"""函数级别夹具,返回测试数据"""return{"a":10,"b":5}# ---------- 测试类 ----------classTestCalculator:"""使用类级别夹具"""deftest_add(self,calculator,sample_data):result=calculator.add(sample_data["a"],sample_data["b"])assertresult==15# pytest 使用原生 assertdeftest_divide_normal(self,calculator):assertcalculator.divide(9,3)==3.0deftest_divide_by_zero(self,calculator):withpytest.raises(ValueError,match="除数不能为零"):calculator.divide(5,0)# ---------- 参数化测试 (pytest 特色) ----------@pytest.mark.parametrize("a,b,expected",[(1,2,3),(-1,-1,-2),(0,0,0),(100,-50,50),])deftest_add_parametrized(a,b,expected):calc=Calculator()assertcalc.add(a,b)==expected# 组合参数化 + 夹具@pytest.mark.parametrize("input_val,expected",[(5,5),(0,0),(-3,-3)])deftest_identity(calculator,input_val,expected):assertcalculator.add(input_val,0)==expected# ---------- 模拟 (使用 mocker fixture,需要安装 pytest-mock) ----------# 也可以直接使用 unittest.mock,pytest 自动兼容deftest_fetch_data_success(mocker):# 使用 pytest-mock 的 mocker fixturemock_get=mocker.patch('calculator.requests.get')mock_response=Mock()mock_response.status_code=200mock_response.json.return_value={"key":"value"}mock_get.return_value=mock_response status,data=fetch_data("http://example.com")assertstatus==200assertdata=={"key":"value"}mock_get.assert_called_once_with("http://example.com")deftest_fetch_data_failure():withpatch('calculator.requests.get')asmock_get:mock_response=Mock()mock_response.status_code=404mock_response.json.side_effect=Exception("Not Found")mock_get.return_value=mock_responsewithpytest.raises(Exception):fetch_data("http://example.com/bad")# ---------- 跳过与预期失败 ----------@pytest.mark.skip(reason="演示跳过,未实现功能")deftest_skip_example():pass@pytest.mark.skipif(True,reason="条件满足时跳过")deftest_skip_conditionally():pass@pytest.mark.xfail(reason="已知问题,暂时预期失败")deftest_expected_failure():assert1==2# ---------- 临时文件与 capsys (pytest 内置 fixture) ----------deftest_capsys_example(capsys):print("Hello, world!")captured=capsys.readouterr()assertcaptured.out=="Hello, world!\n"deftest_tmp_path(tmp_path):d=tmp_path/"sub"d.mkdir()f=d/"test.txt"f.write_text("pytest rocks")assertf.read_text()=="pytest rocks"# ---------- 自定义标记 ----------@pytest.mark.slowdeftest_slow_operation():importtime time.sleep(0.1)assertTrue# 运行方式: pytest -m slow (只运行标记为 slow 的测试)运行方式:
- 安装 pytest:
pip install pytest pytest-mock - 执行:
pytest test_pytest_demo.py -v - 带覆盖率:
pytest --cov=calculator test_pytest_demo.py
关键差异对比
| 特性 | unittest | pytest |
|---|---|---|
| 断言风格 | self.assertEqual(a, b) | assert a == b |
| 夹具 | setUp/tearDown | @pytest.fixture,更灵活 |
| 参数化 | subTest或parameterized | @pytest.mark.parametrize |
| 模拟 | unittest.mock | unittest.mock或mocker |
| 跳过/预期失败 | @unittest.skip/expectedFailure | @pytest.mark.skip/xfail |
| 插件生态 | 较少 | 极丰富 (xdist, cov, asyncio…) |
| 学习曲线 | 较低,但代码冗长 | 稍高,但更简洁强大 |
建议:新项目直接选择pytest,旧项目维护可使用 unittest。两者可混合使用(pytest 能运行 unittest 风格的测试)。