当前位置: 首页 > news >正文

Python单元测试与Mock技术

Python单元测试与Mock技术

一、unittest基础

Python标准库unittest提供了完整的测试框架:

import unittest

class Calculator:
def add(self, a, b):
return a + b

def divide(self, a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b

class TestCalculator(unittest.TestCase):
def setUp(self):
"""每个测试方法执行前调用"""
self.calc = Calculator()

def tearDown(self):
"""每个测试方法执行后调用"""
pass

def test_add(self):
self.assertEqual(self.calc.add(2, 3), 5)
self.assertEqual(self.calc.add(-1, 1), 0)
self.assertEqual(self.calc.add(0, 0), 0)

def test_divide(self):
self.assertAlmostEqual(self.calc.divide(10, 3), 3.333, places=3)
self.assertEqual(self.calc.divide(6, 2), 3)

def test_divide_by_zero(self):
with self.assertRaises(ValueError) as context:
self.calc.divide(1, 0)
self.assertIn("除数不能为零", str(context.exception))

if __name__ == '__main__':
unittest.main()


二、pytest框架

pytest是更现代、更简洁的测试框架:

import pytest

class UserService:
def __init__(self, db):
self.db = db

def get_user(self, user_id):
user = self.db.find_by_id(user_id)
if not user:
raise ValueError(f"用户 {user_id} 不存在")
return user

def create_user(self, name, email):
if not name or not email:
raise ValueError("姓名和邮箱不能为空")
return self.db.insert({'name': name, 'email': email})

# pytest风格的测试 - 更简洁
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5

def test_divide_by_zero():
calc = Calculator()
with pytest.raises(ValueError, match="除数不能为零"):
calc.divide(1, 0)

# fixture:依赖注入
@pytest.fixture
def calculator():
return Calculator()

@pytest.fixture
def sample_data():
return [1, 2, 3, 4, 5]

def test_add_with_fixture(calculator):
assert calculator.add(10, 20) == 30

# 参数化测试
@pytest.mark.parametrize("a, b, expected", [
(1, 1, 2),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add_parametrize(calculator, a, b, expected):
assert calculator.add(a, b) == expected


三、fixture高级用法

@pytest.fixture(scope="module")
def db_connection():
"""模块级fixture,整个模块只创建一次"""
conn = create_connection()
yield conn # yield之后是清理代码
conn.close()

@pytest.fixture(scope="function")
def clean_db(db_connection):
"""每个测试函数前清理数据库"""
db_connection.execute("DELETE FROM test_table")
yield db_connection
db_connection.rollback()

@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database(request):
"""参数化fixture,测试会对每种数据库运行一次"""
db = create_database(request.param)
yield db
db.cleanup()

# 使用conftest.py共享fixture
# conftest.py
@pytest.fixture(autouse=True)
def reset_environment():
"""自动应用到所有测试"""
import os
old_env = os.environ.copy()
yield
os.environ.clear()
os.environ.update(old_env)


四、Mock基础

unittest.mock用于替换测试中的依赖:

from unittest.mock import Mock, patch, MagicMock, call

# 基本Mock
mock_db = Mock()
mock_db.find_by_id.return_value = {'id': 1, 'name': 'Alice'}

service = UserService(mock_db)
user = service.get_user(1)

assert user == {'id': 1, 'name': 'Alice'}
mock_db.find_by_id.assert_called_once_with(1)

# 配置Mock的行为
mock_db.find_by_id.side_effect = [
{'id': 1, 'name': 'Alice'}, # 第一次调用返回
{'id': 2, 'name': 'Bob'}, # 第二次调用返回
ValueError("数据库错误"), # 第三次调用抛异常
]

# Mock属性
mock_config = Mock()
mock_config.database.host = 'localhost'
mock_config.database.port = 5432
print(mock_config.database.host) # 'localhost'


五、patch装饰器

import requests

class WeatherService:
API_URL = "https://api.weather.com"

def get_temperature(self, city):
response = requests.get(f"{self.API_URL}/current", params={'city': city})
if response.status_code != 200:
raise ConnectionError("API请求失败")
data = response.json()
return data['temperature']

# 方式1:装饰器
@patch('requests.get')
def test_get_temperature(mock_get):
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {'temperature': 25.5}
mock_get.return_value = mock_response

service = WeatherService()
temp = service.get_temperature('Beijing')

assert temp == 25.5
mock_get.assert_called_once()

# 方式2:上下文管理器
def test_api_failure():
with patch('requests.get') as mock_get:
mock_get.return_value.status_code = 500

service = WeatherService()
with pytest.raises(ConnectionError):
service.get_temperature('Beijing')

# 方式3:patch.object
def test_with_patch_object():
service = WeatherService()
with patch.object(service, 'API_URL', 'http://test-api.com'):
# 在这个块中API_URL被替换
pass


六、Mock高级技巧

6.1 spec参数

class Database:
def connect(self): pass
def query(self, sql): pass
def close(self): pass

# spec确保Mock只有原始类的方法
mock_db = Mock(spec=Database)
mock_db.connect() # OK
# mock_db.nonexistent() # AttributeError - 防止拼写错误

6.2 PropertyMock

from unittest.mock import PropertyMock

class Config:
@property
def debug(self):
return False

with patch.object(Config, 'debug', new_callable=PropertyMock, return_value=True):
config = Config()
assert config.debug == True

6.3 异步Mock

import asyncio
from unittest.mock import AsyncMock

class AsyncService:
async def fetch(self, url):
pass

@pytest.mark.asyncio
async def test_async_service():
service = AsyncService()
service.fetch = AsyncMock(return_value={'data': 'test'})

result = await service.fetch('http://example.com')
assert result == {'data': 'test'}
service.fetch.assert_awaited_once()

6.4 调用记录验证

mock = Mock()
mock(1, 2, key='value')
mock(3, 4)
mock.method(5)

# 验证调用
mock.assert_any_call(1, 2, key='value')
assert mock.call_count == 2
assert mock.method.call_count == 1

# 验证调用顺序
expected_calls = [call(1, 2, key='value'), call(3, 4)]
mock.assert_has_calls(expected_calls, any_order=False)


七、测试替身策略

7.1 Stub(桩)

def test_with_stub():
"""Stub只返回预设值,不验证交互"""
stub_repo = Mock()
stub_repo.find_all.return_value = [
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'},
]

service = UserService(stub_repo)
users = service.list_users()
assert len(users) == 2

7.2 Spy(间谍)

from unittest.mock import patch

def test_with_spy():
"""Spy记录调用但保留原始行为"""
original_method = Calculator.add

with patch.object(Calculator, 'add', wraps=Calculator().add) as spy:
calc = Calculator()
result = calc.add(2, 3)

assert result == 5 # 原始行为保留
spy.assert_called_once_with(2, 3) # 但调用被记录

7.3 Fake(伪造)

class FakeDatabase:
"""内存数据库,用于测试"""
def __init__(self):
self._data = {}
self._next_id = 1

def insert(self, record):
record['id'] = self._next_id
self._data[self._next_id] = record
self._next_id += 1
return record

def find_by_id(self, record_id):
return self._data.get(record_id)

def delete(self, record_id):
return self._data.pop(record_id, None)

def test_with_fake():
fake_db = FakeDatabase()
service = UserService(fake_db)

created = service.create_user("Alice", "alice@example.com")
assert created['id'] == 1

found = service.get_user(1)
assert found['name'] == 'Alice'


八、测试组织与最佳实践

# 目录结构
# project/
# src/
# services/
# user_service.py
# tests/
# conftest.py
# unit/
# test_user_service.py
# integration/
# test_user_api.py

# conftest.py - 共享fixture
import pytest

@pytest.fixture
def mock_db():
return FakeDatabase()

@pytest.fixture
def user_service(mock_db):
return UserService(mock_db)

# pytest.ini 配置
# [pytest]
# testpaths = tests
# python_files = test_*.py
# python_functions = test_*
# markers =
# slow: 标记慢速测试
# integration: 集成测试

# 使用marker
@pytest.mark.slow
def test_large_dataset():
pass

@pytest.mark.integration
def test_real_database():
pass

# 运行特定标记的测试
# pytest -m "not slow"
# pytest -m integration


九、覆盖率

# 安装: pip install pytest-cov
# 运行: pytest --cov=src --cov-report=html

# .coveragerc 配置
# [run]
# source = src
# omit = */tests/*
#
# [report]
# exclude_lines =
# pragma: no cover
# if __name__ == .__main__.:
# raise NotImplementedError


十、测试原则

1. 每个测试只验证一个行为
2. 测试应该独立,不依赖执行顺序
3. 优先测试行为而非实现细节
4. Mock外部依赖,不Mock被测对象本身
5. 测试命名清晰表达意图:test_应该_当条件时
6. 遵循AAA模式:Arrange(准备)、Act(执行)、Assert(断言)

总结:良好的测试是代码质量的保障。unittest.mock提供了强大的替身工具,pytest让测试编写更简洁。关键是选择合适的测试替身策略:简单场景用Mock,需要真实行为用Fake,需要验证交互用Spy。

http://www.jsqmd.com/news/820753/

相关文章:

  • 自动化测试(十五) 自动化测试平台化-从脚本到CI-CD质量门禁
  • PCF8591模数转换器实战指南:从I2C通信到多通道数据采集
  • 终极Cookie本地导出指南:如何安全获取cookies.txt文件
  • 2026 南京国贸大厦纹眉深度测评:本土直营标杆,纹绣世家 4 大门店技术 / 审美 / 安全全优 - 小艾信息发布
  • 3D打印衍射光栅:低成本实现虹彩表面处理技术
  • 驾驶舱前端设计方案:从“花架子”到“真能用”的组件化实战
  • 2026年成都老牌GEO公司全景解析,权威榜单带你一览行业风采! - 品牌推荐官方
  • 这份「疫苗发布和接种预约系统」源码和论文,适合正在赶项目的同学收藏!
  • ofd.js终极指南:在浏览器中直接渲染OFD文档的完整解决方案
  • 玻璃钢管道技术解析与合规厂家选型实用指南 - 奔跑123
  • 哈尔滨钢结构专项分包工程公司综合实力排行盘点 - 奔跑123
  • 日常记录:SQL学习总结
  • 科技
  • 实战解析:XiaoMusic技术架构深度剖析与智能音箱语音控制实现方案
  • 2026年度盘点!10款好用的降AI工具,AI率一键降至9% - 降AI实验室
  • 这份「基于SpringBoot的疾病防控综合系统」源码和论文,适合做公共卫生类毕设参考!
  • 工业喷淋塔技术选型与实测指南 适配多工况需求 - 奔跑123
  • 天猫超市购物卡如何高价回收? - 团团收购物卡回收
  • JL-01多通道温湿度记录仪:环境监测的得力助手
  • 终极英雄联盟自动BP与战绩查询工具:Seraphine完全指南
  • 工作3年的Python程序员,转大模型开发,我总结的所有实战技巧
  • 终极指南:如何免费解锁WeMod高级功能并增强游戏体验
  • libutp 性能分析总结
  • 你真的理解 volatile 关键字了吗?
  • Spring Boot 3 全局异常处理终极指南(附完整代码架构),拿走即用
  • 如何摆脱游戏卡顿困扰:DLSS Swapper的智能性能管理方案
  • 为什么FreeBSD和苹果都爱用Clang?聊聊它的模块化设计与商业友好性
  • 优雅进程终止:Go工具halt的设计原理与实战应用
  • 泉州 CPPM 认证培训 福建制造业采购必考证书 - 中供国培
  • 全屋定制酒柜技术拆解:从板材到工艺的硬核标准 - 奔跑123