HOOOS

单元测试中,数据库查询和文件读写如何“假装”?Mock 和 Stub 实战指南

0 8 代码老司机 单元测试MockStub
Apple

单元测试中,如何优雅地隔离外部依赖?

在单元测试中,隔离外部依赖至关重要。前辈指出你的单元测试对外部依赖处理不当,导致测试过于耦合和脆弱,这很常见。 隔离依赖可以使测试更快速、更可靠,并且更容易定位问题。 面对数据库查询、文件读写等场景,除了真实调用,我们还能如何“假装”它们的存在,并保证测试的有效性呢?

答案就是使用 MockStub

什么是 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,写出更健壮、更易维护的代码。

点评评价

captcha
健康