单元测试中,如何优雅地隔离外部依赖?
在单元测试中,隔离外部依赖至关重要。前辈指出你的单元测试对外部依赖处理不当,导致测试过于耦合和脆弱,这很常见。 隔离依赖可以使测试更快速、更可靠,并且更容易定位问题。 面对数据库查询、文件读写等场景,除了真实调用,我们还能如何“假装”它们的存在,并保证测试的有效性呢?
答案就是使用 Mock 和 Stub。
什么是 Mock 和 Stub?
- Stub (桩件): 提供预设的返回值,用于替代真实依赖,让测试代码可以顺利执行。 就像一个临时的替身,它很简单,只负责返回预先设定的数据。
- Mock (模拟对象): 除了提供预设返回值外,还能验证方法是否被调用,以及调用次数和参数是否符合预期。 Mock 比 Stub 更强大,它不仅能“假装”,还能“监视”。
数据库查询的“假装”
假设有一个函数 get_user_name(user_id)
需要从数据库中查询用户姓名。
def get_user_name(user_id):
# 真实代码会连接数据库并查询
# 这里简化为直接返回
return database.query("SELECT name FROM users WHERE id = %s", (user_id,))[0][0]
在单元测试中,我们不希望真的连接数据库,可以使用 Mock 来模拟数据库查询。
from unittest.mock import Mock
def test_get_user_name():
# 创建一个 Mock 对象,模拟数据库连接
database_mock = Mock()
# 设置 Mock 对象的返回值
database_mock.query.return_value = [("张三",)]
# 将真实的 database 替换为 Mock 对象 (依赖注入)
# 在实际应用中,可以使用依赖注入框架来实现
global database
original_database = database
database = database_mock
# 调用被测试函数
user_name = get_user_name(123)
# 断言返回值是否符合预期
assert user_name == "张三"
# 验证 database_mock.query 是否被调用,以及调用参数是否正确
database_mock.query.assert_called_once_with("SELECT name FROM users WHERE id = %s", (123,))
# 恢复 database
database = original_database
在这个例子中,我们使用 unittest.mock.Mock
创建了一个 database_mock
对象,并设置了它的 query
方法的返回值。 这样,在 get_user_name
函数中调用 database.query
时,实际上调用的是 database_mock.query
,它会返回我们预设的值,而不会真正连接数据库。 assert_called_once_with
可以验证 database_mock.query
是否被调用,以及调用参数是否正确,确保我们的代码按照预期的方式使用了数据库。
文件读写的“假装”
假设有一个函数 read_config(file_path)
需要读取配置文件。
def read_config(file_path):
with open(file_path, "r") as f:
config = f.read()
return config
同样,在单元测试中,我们不希望真的读取文件,可以使用 Stub 来模拟文件读取。
def test_read_config():
# 创建一个 Stub 对象,模拟文件对象
file_stub = io.StringIO("key1 = value1\nkey2 = value2")
# 替换 open 函数
original_open = __builtins__.open
__builtins__.open = lambda file, mode: file_stub
# 调用被测试函数
config = read_config("dummy_path")
# 断言返回值是否符合预期
assert config == "key1 = value1\nkey2 = value2"
# 恢复 open 函数
__builtins__.open = original_open
在这个例子中,我们使用 io.StringIO
创建了一个 file_stub
对象,它模拟了一个文件对象,并包含了我们预设的配置文件内容。 然后,我们替换了内置的 open
函数,使其返回 file_stub
对象。 这样,在 read_config
函数中调用 open
函数时,实际上返回的是 file_stub
对象,它会返回我们预设的配置文件内容,而不会真正读取文件。
Mock 和 Stub 的选择
- 如果只需要提供预设的返回值,可以使用 Stub。
- 如果需要验证方法是否被调用,以及调用次数和参数是否符合预期,可以使用 Mock。
- 一般来说,Mock 比 Stub 更强大,也更常用。
总结
使用 Mock 和 Stub 可以有效地隔离外部依赖,使单元测试更快速、更可靠。 掌握 Mock 和 Stub 的使用方法,是编写高质量单元测试的关键。 通过“假装”外部依赖,我们可以专注于测试代码的逻辑,而不用担心外部环境的影响。 希望本文能帮助你更好地理解和使用 Mock 和 Stub,写出更健壮、更易维护的代码。