为什么单元测试不该直接调用外部服务
写单元测试时,很多人一开始都会图省事,直接让测试代码去请求真实的 API 接口,比如调用支付网关、短信平台或用户中心服务。看起来没问题,但一旦网络不通、接口限流或返回异常数据,测试就挂了。这不是代码逻辑的问题,而是测试本身变得不可靠。
单元测试的核心是验证函数或类的内部逻辑,而不是检查网络能不能通。如果一个加法函数因为天气服务连不上而失败,那这个测试就没意义。
常见的外部依赖场景
比如你有个订单模块,下单成功后要发短信通知用户。测试里如果真去调短信服务商,可能今天能发,明天被封 IP,测试结果忽好忽坏。再比如获取用户地理位置信息,依赖第三方地图 API,响应慢或者返回空数据,都会导致本地构建失败。
这类问题在团队协作中特别头疼。别人拉下代码跑测试,一堆红,第一反应是“我代码写坏了”,查半天才发现是测试依赖的服务出问题了。
用 Mock 隔离外部依赖
解决办法是把外部服务“假装”成一个可控的对象。Python 里可以用 unittest.mock,Java 有 Mockito,JavaScript 常用 Jest 提供的 mock 功能。
比如一个函数依赖 HTTP 请求获取用户信息:
def get_user_name(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json().get('name')测试时不需要真的发请求,可以 mock 掉 requests.get:
@patch('requests.get')
def test_get_user_name(mock_get):
mock_get.return_value.json.return_value = {'id': 1, 'name': '张三'}
result = get_user_name(1)
assert result == '张三'
mock_get.assert_called_once()这样无论外网是否正常,测试都能稳定运行。
使用测试替身:Stub 和 Fake
除了 Mock,还可以用 Stub 提供固定返回值。比如写个假的短信发送器,调用时只记录“已发送”,并不真发短信。
Fake 则是轻量实现,比如用内存字典模拟数据库操作,避免启动真实 MySQL 或 Redis。既快又干净,适合高频执行的单元测试。
环境变量控制真实调用
有些情况确实需要走一遍真实接口,比如做集成测试。这时候应该和单元测试分开,通过环境变量控制。
比如设置 TEST_ENV=integration 时才启用网络请求,平时跑单元测试默认关闭。CI/CD 流水线里也可以分两步:先跑纯单元测试,再跑带外部依赖的集成测试。
别忘了清理和重置
Mock 用完记得清理,不然可能影响后续测试。很多框架支持自动重置,比如 Jest 的 afterEach 清理 spy,Python 的 patch 装饰器会自动还原原对象。
如果多个测试共用同一个 mock 状态,容易出现前一个测试改了行为,后一个测试莫名其妙失败。这种问题不好查,最好每个测试独立隔离。
实际项目中的建议
新项目一开始就定好规范:单元测试不能有网络请求、不能读写本地文件、不能依赖系统时间。老项目可以逐步改造,先把最不稳定的测试用例 mock 起来。
团队里可以加一条 CI 规则:任何提交的测试代码如果包含对外部域名的直接调用,自动拒绝合并。用工具扫描测试文件里的 http:// 或 requests. 调用,提醒开发者改造成 mock。
稳定可靠的测试才能真正帮我们发现问题,而不是制造噪音。