pandas 完整实践:销售质量校验与客户月报 · 错误样本与测试规格至综合练习
前置:连接与分组。目标是把销售事实表与客户地区维度合并,在上海时区按月统计订单数与总分。所有金额和日期规则都在进入聚合前验证;发现坏数据就不发布整份报告。示例未执行,以下输出是预期结果,静态检查不等于运行正确。
错误样本与测试规格
pandas月报应测试哪些失败:重复销售、重复客户、未知客户、金额非法、空ID、无偏移时间及超出当地年份都必须拒绝,并断言既有输出内容不变。测试还用mock令替换失败,核对临时文件清理;正常场景按字节对比固定CSV。这些是待读者执行的行为规格,当前仅验证测试源码AST能解析。
错误输入:逐一替换对应销售或客户文件
text
customer_id,region
001,east
001,westtext
sale_id,customer_id,sold_at,amount_cents
s01,999,2025-01-31T15:59:59Z,1000text
sale_id,customer_id,sold_at,amount_cents
s01,001,2025-01-31T15:59:59Z,1.25text
sale_id,customer_id,sold_at,amount_cents
s01,001,2025-01-31T15:59:59,1000text
sale_id,customer_id,sold_at,amount_cents
s01,,2025-01-31T15:59:59Z,1000text
sale_id,customer_id,sold_at,amount_cents
s01,001,2025-12-31T16:00:00Z,1000text
sale_id,customer_id,sold_at,amount_cents
s01,001,2025-01-31T15:59:59Z,1000
s01,001,2025-01-31T15:59:59Z,1000test_report.py:完整测试规格,未执行
python
"""Behavior specifications; 示例未执行. Run only in a prepared environment."""
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
from unittest.mock import patch
from report import build_report, main, publish
HERE = Path(__file__).resolve().parent
class SalesReportTests(unittest.TestCase):
def test_golden_cli(self):
with TemporaryDirectory() as folder:
target = Path(folder) / 'monthly.csv'
self.assertEqual(main([str(HERE / 'input/sales.csv'),
str(HERE / 'input/customers.csv'), str(target)]), 0)
self.assertEqual(target.read_bytes(), (HERE / 'expected/monthly.csv').read_bytes())
def test_invalid_input_preserves_report(self):
for name in ['unknown-customer', 'invalid-money', 'naive-time', 'missing-id',
'outside-year', 'duplicate-sale', 'duplicate-customers']:
with self.subTest(name=name), TemporaryDirectory() as folder:
target = Path(folder) / 'report.csv'
target.write_bytes(b'previous report\n')
sales = HERE / ('input/sales.csv' if name == 'duplicate-customers'
else f'invalid/{name}.csv')
customers = HERE / ('invalid/duplicate-customers.csv' if name == 'duplicate-customers'
else 'input/customers.csv')
self.assertEqual(main([str(sales), str(customers), str(target)]), 1)
self.assertEqual(target.read_bytes(), b'previous report\n')
self.assertEqual(list(Path(folder).glob('.sales-*.tmp')), [])
def test_replace_failure_removes_temporary(self):
report = build_report(HERE / 'input/sales.csv', HERE / 'input/customers.csv')
with TemporaryDirectory() as folder:
target = Path(folder) / 'report.csv'
target.write_bytes(b'old\n')
with patch('report.os.replace', side_effect=OSError('locked')):
with self.assertRaises(OSError):
publish(report, target)
self.assertEqual(target.read_bytes(), b'old\n')
self.assertEqual(list(Path(folder).glob('.sales-*.tmp')), [])
def test_schema_and_bounds(self):
header = 'sale_id,customer_id,sold_at,amount_cents\n'
cases = [header, 'wrong,header\n', header + 's,001,2025-01-01T00:00:00Z,1,extra\n',
header + 's,001,2025-02-30T00:00:00Z,1\n',
header + 's,001,2025-01-01T00:00:00Z,1000000000001\n',
header + 's,001,2025-01-01T00:00:00Z,-1\n']
for source in cases:
with self.subTest(source=source), TemporaryDirectory() as folder:
path = Path(folder) / 'sales.csv'
path.write_text(source, encoding='utf-8')
with self.assertRaises(ValueError):
build_report(path, HERE / 'input/customers.csv')
def test_destination_cannot_be_input(self):
with TemporaryDirectory() as folder:
sales = Path(folder) / 'sales.csv'
original = (HERE / 'input/sales.csv').read_bytes()
sales.write_bytes(original)
self.assertEqual(main([str(sales), str(HERE / 'input/customers.csv'), str(sales)]), 1)
self.assertEqual(sales.read_bytes(), original)
if __name__ == '__main__':
unittest.main()综合练习
先不改代码,手算五笔销售在上海月份下的分组。再设计一条能通过时间正则、但必须在日期解析失败的输入。最后若要允许退款,列出需要改的校验和测试,不只删除一个负号检查。
参考解法
一月east1000;二月east2000、unknown0、west3000;三月west1500,每组一笔。2025-02-30T00:00:00Z格式完整但日期不存在。退款需要带符号语法、业务身份与冲销关联、分组正负累计上界和净额口径;当前非负全局和保护分组的证明会失效,应先形成新契约再实施。
继续阅读
- 知识专题
- 排错
- 排错