在 Python 数据处理项目中,经常会遇到这样的场景:某个业务函数内部直接通过 pd.DataFrame(...) 创建了表格对象,并基于它做了清洗或聚合,最后只把统计结果返回给调用方。当我们想为这个函数写单元测试时,往往只能断言最终的标量或汇总数据,却没法验证函数内部那张中间表是否符合预期,比如列名拼错、某列类型变成 object 等隐患就会被掩盖。

为什么直接断言返回值不够
假设被测函数从数据库读出一个列表,然后在内部拼成 DataFrame 做处理。如果只检查返回值,一旦内部用了错误的字段名构造 DataFrame,只要后续聚合恰好没报错,测试就会绿,但生产数据稍微一变就可能抛异常。我们希望在测试里拿到那个“藏”在函数里的 DataFrame 实例,去检查它的 columns、shape 和 dtypes。
常见的误区是改生产代码,把 DataFrame 作为参数传进去。这确实能测,但侵入性强,且破坏了原有封装。更轻量的做法是在测试期间临时替换 pandas 的构造函数,把创建的实例统统记下来,函数跑完后再从记录里取用。
用猴子补丁捕获内部实例
核心思路是:在测试执行前,保存原始的 pd.DataFrame,然后赋给一个包装函数,它调用原始构造器得到实例,并追加到外部列表,最后返回该实例。这样函数内部任何 pd.DataFrame(...) 调用都会经过我们的包装层。
下面示例用 unittest 演示。我们用 setUp 做补丁,tearDown 还原,保证不会影响其他测试。
import unittest
import pandas as pd
class TestCaptureDF(unittest.TestCase):
def setUp(self):
self.created = []
self._orig_df = pd.DataFrame
def spy_df(*args, **kwargs):
inst = self._orig_df(*args, **kwargs)
self.created.append(inst)
return inst
pd.DataFrame = spy_df
def tearDown(self):
pd.DataFrame = self._orig_df
def test_internal_df(self):
def build_report(rows):
df = pd.DataFrame(rows)
return df['amount'].sum()
result = build_report([{'amount': 10}, {'amount': 20}])
self.assertEqual(result, 30)
self.assertEqual(len(self.created), 1)
inner_df = self.created[0]
self.assertIn('amount', inner_df.columns)
self.assertEqual(inner_df.shape[0], 2)
if __name__ == '__main__':
unittest.main()
上面的 spy_df 就是我们的探针。注意在 tearDown 里把 pd.DataFrame 还原,否则其他测试用例如果引入同一 pandas 模块,会持续走包装逻辑,造成用例间相互污染。
这种写法的优点是零侵入、可读性好;缺点是所有 DataFrame 创建都会被记录,如果被测函数还顺手建了别的表,你需要按索引或特征去筛选。可以通过给 spy 增加调用栈判断来只捕获特定上下文的实例。
更安全的上下文管理器写法
如果不想依赖 setUp/tearDown,可以用上下文管理器把补丁范围缩到最小,避免忘记还原。这样在代码评审时也能一眼看出哪段测试在捕获实例。
import pandas as pd
from contextlib import contextmanager
@contextmanager
def capture_dataframes():
created = []
orig = pd.DataFrame
def spy(*args, **kwargs):
inst = orig(*args, **kwargs)
created.append(inst)
return inst
pd.DataFrame = spy
try:
yield created
finally:
pd.DataFrame = orig
def test_with_context():
def inner_func():
return pd.DataFrame({'x': [1, 2]})
with capture_dataframes() as dfs:
inner_func()
assert len(dfs) == 1
assert list(dfs[0].columns) == ['x']
上下文管理器在 yield 之后必定执行 finally 还原,即使测试断言失败也不会漏掉恢复动作。对于 pytest 用户,也可以封装成 fixture 实现同样效果。
当函数内部使用 pd.DataFrame.from_dict 等类方法时,上面的补丁不会生效,因为那不是直接调用 pd.DataFrame 构造器。此时可以额外替换对应的类方法,或者统一在测试里 mock 整个 pandas 模块的相应入口。
筛选与断言建议
拿到实例列表后,建议根据业务特征过滤,比如只保留列包含特定字段的表,再进行断言。这样即使函数重构时多建了临时表,测试依然稳定。
def find_df_with_column(dfs, col):
for d in dfs:
if col in getattr(d, 'columns', []):
return d
return None
# 使用示例
# target = find_df_with_column(captured, 'user_id')
# assert target['user_id'].dtype == 'int64'
通过这种捕获方式,我们能把单元测试从“只测输出”升级到“观测过程”,更早暴露数据形变问题。它尤其适合遗留代码补全测试的场景,不需要大改函数签名就能补齐对中间结构的保护。
unittestpandasDataFrame_capture修改时间:2026-08-04 23:24:28