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

Django TestCase原子事务失败时数据库未回滚问题求助

原子事务单元测试异常问题排查

问题背景

要实现账户间转账的原子事务单元测试,通过transaction.atomic()保证操作要么全部成功要么全部失败。预期验证:当接收方deposit函数失败时,发送方的扣款操作会回滚,余额保持不变。但测试用例失败,显示发送方余额已减少;尝试替换TransactionTestCase无效,仅VS Code逐行调试时测试能通过。

原代码片段

import pytest
from cards.models import Card, Account
from unittest.mock import patch

def perform_atomic(txn, sender_account, receiver_account, amount):
    try:
        with transaction.atomic():
            sender_account.balance -= 1000
            sender_account.save()
            receiver_account.deposit(amount)
    except Exception as error:
        txn.status = Transaction.TRANSACTION_STATUS_FAILED
        txn.save()
        logger.error(f"Failed Action. Reason[{error}]", exc_info=True)
        raise error  

# Test case
@patch.object(Account, "deposit")
def test_atomicity_when_deposit_fails(self, deposit_mock):
    deposit_mock.side_effect = ValidationError("deposit failed")
    self.sender_account.balance = 300.0
    self.sender_account.save()

    # Balance before attempting to transfer
    before_sender_balance = self.sender_account.balance
    with pytest.raises(ValidationError, match="deposit failed"):
        perform_atomic(self.transaction, self.sender_account, self.receiver_account, self.amount)

    self.sender_account.refresh_from_db()

    # Balance after attempting to transfer
    after_sender_balance = self.sender_account.balance.amount
    
    # assert that the sender balanc didn't change
    assert before_sender_balance == after_sender_balance

问题原因与修复方案

1. 硬编码扣款金额与测试场景冲突

原代码中硬编码sender_account.balance -= 1000,但测试里发送方初始余额仅300.0,直接减1000会导致余额负数,可能触发额外未捕获异常,干扰事务回滚逻辑。需改为使用传入的amount参数。

2. 断言时类型不匹配

before_sender_balance取的是balance对象,after_sender_balance取的是balance.amount数值,两者类型不同导致断言失败,需统一取值方式。

3. 事务内对象状态与数据库同步问题

调试时执行速度慢,对象缓存与数据库状态同步及时;正常运行时事务回滚后,需确保通过refresh_from_db()正确刷新对象状态(原测试已调用,但需配合前两点修复)。

修正后的代码

事务执行函数

import pytest
from django.db import transaction
from django.core.exceptions import ValidationError
from cards.models import Account, Transaction
import logging

logger = logging.getLogger(__name__)

def perform_atomic(txn, sender_account, receiver_account, amount):
    try:
        with transaction.atomic():
            # 使用传入的amount替代硬编码值
            sender_account.balance -= amount
            sender_account.save()
            receiver_account.deposit(amount)
        # 转账成功时更新事务状态
        txn.status = Transaction.TRANSACTION_STATUS_SUCCESS
        txn.save()
    except Exception as error:
        txn.status = Transaction.TRANSACTION_STATUS_FAILED
        txn.save()
        logger.error(f"Failed Action. Reason[{error}]", exc_info=True)
        raise error

测试用例

@patch.object(Account, "deposit")
def test_atomicity_when_deposit_fails(self, deposit_mock):
    deposit_mock.side_effect = ValidationError("deposit failed")
    self.amount = 100.0  # 设置合理的转账金额,小于初始余额
    self.sender_account.balance = 300.0
    self.sender_account.save()

    # 统一取数值类型
    before_sender_balance = self.sender_account.balance.amount
    with pytest.raises(ValidationError, match="deposit failed"):
        perform_atomic(self.transaction, self.sender_account, self.receiver_account, self.amount)

    self.sender_account.refresh_from_db()
    after_sender_balance = self.sender_account.balance.amount
    
    assert before_sender_balance == after_sender_balance

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 16:35:50