You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对含副作用的函数(如数据库操作函数)开展单元测试?

针对数据库交互函数的单元测试优化方案

我非常理解你现在的困扰——仅仅mock数据库连接然后检查execute的调用情况,确实测不到核心的业务逻辑,很容易出现“测试通过但实际代码有问题”的情况。下面我给你两种可行的思路,既满足你偏好单元测试的需求,又能提升测试的有效性:

一、不依赖检查副作用调用参数的单元测试方法:用带状态的测试替身替代简单Mock

与其只验证cnx.execute的调用次数和参数,不如写一个模拟真实数据库行为的测试替身,它内部维护一个内存中的数据结构来模拟数据库表,执行SQL查询时实际操作这个内存结构。这样你可以直接测试业务操作的结果(比如账户余额变化),而不用纠结底层的调用细节。

举个具体的实现例子:

class MockDBConnection:
    def __init__(self):
        # 用字典模拟账户表,键是账户ID,值是余额
        self.accounts = {"1": 1000, "2": 500}

    def execute(self, query, *args):
        # 根据不同的查询语句模拟对应的数据库操作
        if query == 'query 1':
            # 模拟查询账户余额的逻辑
            return self.accounts[args[0]]
        elif query == 'query 2':
            # 模拟转账更新余额的逻辑
            target_account = args[0]
            amount = args[1]
            # 这里假设query1已经获取了转出账户的余额,所以直接操作
            self.accounts["1"] -= amount
            self.accounts[target_account] += amount
            return True

然后你的单元测试就可以这么写:

def test_dao_transfer():
    # 初始化带状态的模拟连接
    mock_cnx = MockDBConnection()
    # 执行转账操作
    result = dao_transfer(mock_cnx, "1", "2", 200)
    # 直接检查模拟数据库中的状态,验证业务结果
    assert mock_cnx.accounts["1"] == 800
    assert mock_cnx.accounts["2"] == 700
    assert result is True

这种方式的优势在于:你测试的是业务逻辑的实际效果,而不是函数调用的细节。哪怕以后dao_transfer里的SQL语句调整了,只要业务逻辑不变,测试依然能通过,不会因为实现细节的变动而频繁修改测试用例。

二、更利于单元测试的纯IO处理方案:分离业务逻辑与IO操作

如果可以对现有代码做重构,最理想的方式是把核心业务逻辑和数据库IO操作完全分离——让业务逻辑成为不依赖任何外部资源的纯函数,IO操作单独抽成数据访问层。这样单元测试的难度会大幅降低,而且测试的稳定性也会提升。

具体拆分步骤如下:

  1. 提取纯业务逻辑函数:只处理计算和规则校验,不涉及任何数据库操作
def calculate_transfer_balances(balance_from, balance_to, transfer_amount):
    # 这里可以添加各种业务规则校验,比如余额是否充足、转账金额是否合法
    if transfer_amount <= 0:
        raise ValueError("Transfer amount must be positive")
    if balance_from < transfer_amount:
        raise ValueError("Insufficient balance in source account")
    # 返回计算后的新余额
    return balance_from - transfer_amount, balance_to + transfer_amount
  1. 拆分数据库IO操作:把和数据库交互的逻辑单独封装成函数
def get_account_balance(cnx, account_id):
    return cnx.execute('query 1', account_id)

def update_account_balance(cnx, account_id, new_balance):
    return cnx.execute('query 2', account_id, new_balance)
  1. 重构原函数为协调者:只负责调用纯业务函数和数据访问函数,不包含核心逻辑
def dao_transfer(cnx, account_id1, account_id2, money):
    # 获取账户当前余额(IO操作)
    balance1 = get_account_balance(cnx, account_id1)
    balance2 = get_account_balance(cnx, account_id2)
    # 计算新余额(纯业务逻辑)
    new_balance1, new_balance2 = calculate_transfer_balances(balance1, balance2, money)
    # 更新数据库(IO操作)
    update_account_balance(cnx, account_id1, new_balance1)
    update_account_balance(cnx, account_id2, new_balance2)
    return True

这样拆分后,单元测试会变得非常清晰:

  • 对于calculate_transfer_balances,你可以轻松测试各种业务场景(余额充足、余额不足、金额为负等),完全不需要mock任何东西,因为它是纯函数,输入确定输出就确定。
  • 对于dao_transfer,你只需要mockget_account_balance和update_account_balance,验证它们被正确调用即可,不用关心底层的cnx.execute细节;或者结合前面的测试替身,测试完整流程的正确性。

这种架构的好处是:核心业务逻辑的测试独立且稳定,不会因为数据库连接、SQL语句的变化而失效;IO操作的测试可以单独做集成测试,和单元测试互不干扰。

内容的提问来源于stack exchange,提问作者amirouche

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:27:12