← 编程起步:做一个学习记录小助手

第 08 课:用证据说明“我做对了”

学习目标:运行行为测试、读懂失败原因,独立实现筛选功能,并交付可复现的使用说明。
前置:能追踪第 07 课的一条 add 命令。预计 70 分钟,可分两次。
本课交付:通过验收的 practice/task.py,以及自己的项目数据流说明。

本课主线:修改之后,旧能力是否仍然正确

工具现在可用,但“这次跑通”只能证明这组输入没出错。我们需要重复检查正常输入、空数据、边界和错误输入,而不是每次凭记忆点一遍。

测试 = 准备输入 → 执行动作 → 比较实际与预期。预期来自需求,不是从实现代码里照抄一个答案。

先运行项目本身的测试

在 starter-lab 目录:

py -3 -m unittest discover -s tests -v

预期看到 6 个测试方法通过,末尾为 OK。其中的子测试还覆盖多种非法值;6 不代表仅有 6 个输入。

测试包括:分组求和、空记录、校验边界、非法保存不改旧文件、坏 JSON、在全新临时目录调用 CLI 并跨进程读回。临时目录由测试创建和清理,不使用你的 my-records.json。

读懂一条断言

self.assertEqual(
    summarize([]),
    {"count": 0, "total_minutes": 0, "by_subject": {}},
)

左边是实际调用,右边是根据“没有记录”的需求写出的预期。失败时应该先看两边具体差异,再定位实现,不能为了变绿就把预期改成错误结果。

测试代码放在测试类中,因此有 self。此时你可以先把它理解为 unittest 提供的一组断言方法;不要求在本课先精通完整面向对象体系。

独立任务:只保留足够长的学习记录

打开 practice/task.py。它有一个待完成函数:

def select_records(records, minimum):
    """返回 minutes >= minimum 的记录,不修改输入;minimum 为 0–240 的整数。"""
    raise NotImplementedError("请按第 08 课完成筛选与 minimum 参数校验")

输入 records 假定已经通过第 06 课校验;你只需检查 minimum,然后筛选。规定:保留输入顺序,不修改原列表,阈值包含等于;minimum 允许 0,因为“0 表示不过滤”有意义;仍拒绝 True、数字字符串和小数。

输入情形预期行为
25、15、20;阈值 20返回第 1、3 条,顺序不变
阈值 0返回全部记录
阈值 240样例没有记录满足,返回 []
空列表返回 []
阈值 -1、241、True、"20"、1.5抛 ValueError

先看到失败,再自己实现

运行:

py -3 practice/check.py

还没动手时应失败,原因是 NotImplementedError。这是明确标记的学生练习起点,不是课程工具坏了。

实现建议:第一步检查 minimum;第二步创建空列表;第三步遍历 records,满足条件就 append 到新列表;最后在循环外 return。先自己写,再逐项比对需求表。

完成后重复同一命令,预期 3 个测试方法通过。不要改 check.py 来放宽条件。

参考解与解释

卡住时分级看提示:先回看 03 课 if + for,再看 06 课的整数校验,最后才打开 practice/reference.py。

参考实现可以单独验收:

py -3 practice/check.py --reference

该命令检查参考解,不会检查或修改 task.py。参考解通过不代表你的答案通过。

为什么“返回新列表”很重要?如果边遍历边删除原列表的元素,索引会移动,容易跳过数据,也会使调用方手中的记录发生意外变化。先收集满足条件的元素,意图和边界更清楚。

本题只要求筛选动作不修改输入,并不要求深复制每个字典。返回的新列表与原列表仍可引用相同记录;需要完全隔离后续修改时,应另行约定复制行为。

故障演练与排错

将自己的条件临时从 >= 改为 >,再运行验收。包含 20 分钟的测试应失败。恢复后通过,说明测试确实能拦住这个边界错误。

若运行显示 0 tests,不算通过:检查当前目录、命令和 tests 文件夹。若出现 import 错误,先确认你没有把文件命名为 json.py、argparse.py 等标准库同名文件。

最终交付清单

  1. 保存自己的 task.py,让默认 practice/check.py 通过。
  2. 再运行项目测试,确认 6 个方法仍通过。
  3. 写出一条 add 命令与一条 summary 命令,以及预期文件位置。
  4. 画出“参数 → 校验 → 读文件 → 追加 → 写文件”的调用链。
  5. 解释一个仍未解决的边界,例如多进程同时写文件,并指出未来需要学什么。

建议评分:行为与边界 40%,可复现运行 20%,测试与错误定位 20%,数据流解释 20%。代码短不自动加分,能够解释才算完成。

接回代码课堂

你已经亲手处理过数据、规则、文件、入口和测试。去读《代码课堂》01 时,把 main、summarize、load_records、save_records 映射成入口、业务、读取、保存;02 课只是把“终端参数”换成“HTTP 请求”。

沿 01 → 02 → 04 → 05 → 08 → 12 的 Web 主线继续。每次换教材仍用同一个问题:这次操作从哪里进来,数据在哪里变,失败在哪里返回,我怎样验证它?