quality-code-review

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Quality Code Review

高质量代码审查

A reviewer's checklist for Frappe applications. Protect correctness, security, and the future maintainer, in that order of consequence. The sections below run in that order — spend most attention on §1 and §2. Prefer a root-cause fix over a workaround, and say why a finding matters (what breaks, for whom).

针对Frappe应用的审查者清单。优先保障正确性、安全性,以及未来维护者的工作体验,三者的重要性依次递减。以下章节也按此顺序排列——请重点关注第1节和第2节。优先选择从根源解决问题而非临时workaround,并说明问题的影响(会导致什么故障、影响哪些用户)。

1. Correctness & stability (highest consequence)

1. 正确性与稳定性(最高优先级)

The worst bug is silent stateful corruption — wrong ledgers/stock posted with no error. Treat stateful and legal/accounting/compliance code as "failure is not an option" code.
  • Fail early and loudly. Use assertions for internal invariants:
    assert total_credit == total_debit
    . (Assertions are for invariants the code guarantees — not user-facing validation.)
  • Picture how it breaks. For every change ask: Where can this break? How will someone misuse this? Write fool-proof code. Extensions and overrides should especially consider handling all sorts of failure modes.
  • No partial commits. A stray
    frappe.db.commit()
    /
    db.rollback()
    mid-transaction ends the transaction and exposes partial state — flag every one. Submitting/saving with validation bypassed (docs posted with no SL/GL entry when validation fails) is a critical bug.
  • Don't sacrifice atomicity for convenience (e.g. adding
    autocommit
    to fix bootstrapping makes transactions non-atomic). Autocommit belongs only on schema creation.
  • Preserve invariants over UX. "Compromise UX, but guarantee correctness." Don't degrade working code to accommodate broken code.
  • Validate the issue before fixing it. Sometimes the correct fix is "don't fix this". Identify the root causes first.
  • Watch for destructive DB APIs with empty/
    None
    filters.
    set_value("Site", None, ...)
    /
    db.delete
    with no filter updates/deletes every row. These must error, not silently operate on the whole table. Flag any
    set_value
    /
    delete
    /
    get_value
    where the name/filter could be
    None
    or empty- or attacker-controlled.
  • Check the types in a condition actually match. A comparison between mismatched types (string vs
    datetime
    , string vs int) silently never matches or is always true — cast explicitly (
    cint
    /
    flt
    ) at the boundary.
  • Question the return shape. Before indexing a result, ask whether it can be
    None
    /
    [None, None]
    ; watch
    as_dict=1
    (list of dicts) vs scalar confusion.
  • Don't silently change long-standing semantics. Behavior callers have relied on for a long time is a contract — altering it is a breaking change in disguise, even when no signature changed.
最严重的bug是无提示的状态损坏——错误的台账/库存记录已提交却未触发任何错误。将涉及状态、法务/会计/合规的代码视为**“不容出错”**的代码。
  • 尽早且明确地抛出错误。使用断言确保内部不变量:
    assert total_credit == total_debit
    。(断言用于保障代码自身承诺的不变量,而非面向用户的验证。)
  • 预判故障场景。针对每一处变更思考:*这里可能在哪里出问题?用户会如何误用?*编写防错代码。扩展和重写逻辑时尤其要考虑所有可能的故障模式。
  • 禁止部分提交。事务中途出现的
    frappe.db.commit()
    /
    db.rollback()
    会终止事务并暴露部分状态——需标记每一处此类代码。绕过验证提交/保存(验证失败时仍提交文档却未生成SL/GL条目)属于严重bug。
  • 不要为了便利牺牲原子性(例如添加
    autocommit
    来解决启动问题会导致事务失去原子性)。Autocommit仅应用于创建数据库表结构的场景。
  • 优先保障不变量而非用户体验。“可以牺牲用户体验,但必须保证正确性。”不要为了适配有问题的代码而破坏原本正常的代码。
  • 修复前先验证问题。有时正确的处理方式是“不修复此问题”。先找到问题的根源。
  • 警惕使用空/
    None
    过滤器的破坏性数据库API
    set_value("Site", None, ...)
    /
    db.delete
    在无过滤器时会更新/删除所有行。此类操作必须触发错误,而非静默操作整个表。标记所有
    set_value
    /
    delete
    /
    get_value
    中名称/过滤器可能为
    None
    、空值或受攻击者控制的情况。
  • 检查条件中的类型是否匹配。不同类型间的比较(字符串 vs
    datetime
    、字符串 vs 整数)会静默地永远不匹配或始终为真——需在边界处显式转换(使用
    cint
    /
    flt
    )。
  • 质疑返回值的结构。在索引结果前,确认它是否可能为
    None
    /
    [None, None]
    ;注意区分
    as_dict=1
    (字典列表)与标量值的混淆。
  • 不要静默更改长期存在的语义。调用者长期依赖的行为属于一种契约——即使函数签名未变,修改该行为也属于隐性的破坏性变更。

2. Security

2. 安全性

Avoiding a vulnerability is far easier than fixing one safely. Audit security-critical code (auth, authorization, permissions, user management) especially hard.
Injection
  • NEVER build SQL by string concatenation/f-strings. Use the ORM or query builder. If raw SQL is unavoidable, use parameter substitution (
    frappe.db.sql("... where name = %s", (user,))
    ) — never interpolate yourself. Better still, avoid introducing new raw SQL at all: beyond injection risk it ties code to one database, and the framework aims to stay DB-agnostic (Postgres support). Prefer
    frappe.qb
    .
  • Type confusion is an injection vector even with the ORM. Frappe accepts complex types, so a parameter expected to be a string can arrive as a filter list:
    {"key": ["!=", ""]}
    passed to
    db.get_value
    bypasses a secret-key check. Validate input types at trust boundaries — explicit
    isinstance(key, str)
    . Audit every
    @frappe.whitelist
    method for this.
  • Never
    eval
    /
    exec
    anything yourself.
    safe_eval
    /
    safe_exec
    only, in limited volume, and "safe_exec is not magic." Never accept a client-supplied method path to execute.
Sandboxing & trust boundaries
  • Sandboxed execution (RestrictedPython/
    safe_exec
    ) is not reliably safe — assume escapes exist. Security toggles must live at the right trust boundary: server-script enablement is a bench-level config, never site-level (a tenant could enable it and take over the whole server).
  • Prefer allowlists over blocklists — blocklists are bypassable. Don't expose everything by default.
Access control
  • "Think 10 times before
    allow_guest=True
    " — it is not a shortcut around real authn/authz. Web pages must apply permissions before reading/sharing data. Prefer
    get_list
    /
    get_all
    over hand-rolled queries.
  • Scope relaxations precisely. Verify a rate-limit/permission exception targets exactly the intended principal — not, say, all non-guest users.
Path traversal / filesystem
  • Prefer the File doctype API. If user input enters a path, ensure it can't traverse (
    /../../
    ) outside the site folder.
Crypto / secrets
  • Never roll your own crypto; reuse existing implementations. Verify authenticity of guest/webhook requests (HMAC).
  • Signed/one-time URLs: use a truly secret signing value; expire by both time and first use; validate using the URL alone, not merged form data (Frappe merges URL + form data → replay attacks with one valid signature).
  • Store secrets in password fields; never plain text; never leak secrets in logs or error messages.
XSS & the rest of OWASP
  • Don't inject user input into the DOM. Treat XSS as critical even when it looks trivial — HTML/JS injection usually leads to account hijack.
  • Don't fix XSS by sanitizing and throwing away special characters. Prefer escaping right before injecting values in DOM.
避免漏洞比安全修复漏洞容易得多。需重点审查安全关键代码(认证、授权、权限、用户管理)。
注入攻击
  • 绝对不要通过字符串拼接/f-strings构建SQL语句。使用ORM或查询构建器。如果必须使用原生SQL,请使用参数替换(
    frappe.db.sql("... where name = %s", (user,))
    )——绝对不要自行插值。更好的做法是:完全避免引入新的原生SQL:除了注入风险,还会将代码绑定到特定数据库,而Frappe框架旨在保持数据库无关性(支持Postgres)。优先使用
    frappe.qb
  • 即使使用ORM,类型混淆也是注入向量。Frappe接受复杂类型,因此预期为字符串的参数可能会以过滤器列表的形式传入:
    {"key": ["!=", ""]}
    传入
    db.get_value
    会绕过密钥检查。在信任边界处验证输入类型——显式使用
    isinstance(key, str)
    。审查每一个
    @frappe.whitelist
    方法是否存在此类问题。
  • 绝对不要自行使用
    eval
    /
    exec
    。仅在有限场景下使用
    safe_eval
    /
    safe_exec
    ,且“safe_exec并非万能”。绝对不要接受客户端提供的方法路径来执行。
沙箱与信任边界
  • 沙箱执行(RestrictedPython/
    safe_exec
    )并非绝对安全——假设存在逃逸漏洞。安全开关必须设置在正确的信任边界:服务器脚本的启用应是bench级别的配置,而非站点级(租户可能启用它并接管整个服务器)。
  • 优先使用白名单而非黑名单——黑名单容易被绕过。不要默认暴露所有内容。
访问控制
  • “使用
    allow_guest=True
    前请三思”——这不是绕过真正认证/授权的捷径。网页必须在读取/共享数据前应用权限检查。优先使用
    get_list
    /
    get_all
    而非手动编写查询。
  • 精确放宽权限范围。验证速率限制/权限例外是否精准针对预期主体——而非例如所有非访客用户。
路径遍历 / 文件系统
  • 优先使用File文档类型API。如果用户输入涉及路径,确保无法通过
    /../../
    遍历到站点文件夹之外。
加密 / 密钥
  • 绝对不要自行实现加密逻辑;复用现有实现。验证访客/ webhook请求的真实性(使用HMAC)。
  • 签名/一次性URL:使用真正保密的签名值;同时通过时间和首次使用来过期;仅使用URL本身进行验证,不要合并表单数据(Frappe会合并URL + 表单数据 → 存在有效签名的重放攻击风险)。
  • 将密钥存储在密码字段中;绝对不要明文存储;绝对不要在日志或错误信息中泄露密钥。
XSS及其他OWASP风险
  • 不要将用户输入注入DOM。即使看似轻微的XSS也需视为严重问题——HTML/JS注入通常会导致账户劫持。
  • 不要通过清理并丢弃特殊字符来修复XSS。优先在将值注入DOM前进行转义。

3. Performance is correctness

3. 性能即正确性

The cheapest time to fix performance is at review; slow code merged sits undetected for years. Performance is a feature (Doherty threshold; humans perceive ~100ms).
  • Budgets: common reads < 100ms; reads < 1s; most writes < 5s; never exceed ~10s. P99 of a frequent read-only request should be ~1s. A slow synchronous request blocks a worker (head-of-line blocking).
  • Complexity rule: a frequently-called endpoint must do O(1) or O(log N) work — a large constant factor at worst, never O(N). Counting rows is O(N), not O(1);
    COUNT(*)
    over a large/filtered table is expensive. Bound unbounded scans (e.g. last 3 months, a "1000+" sentinel) rather than scanning everything.
  • Indexes are code. Flag any
    WHERE
    /join/filter on an unindexed column. Indexes (and custom indexes) must be committed in code, not applied ad-hoc — they get lost on migration otherwise. Form loads that pull comments/versions/assignments need all those queries indexed; one unindexed query makes everything sluggish.
  • No DB calls in loops. "Don't write validations that call db in LOOPS." Flag N+1 patterns. Cache stable values (UOM, docstatus, status) instead of re-querying. This is acceptable in background jobs, but never in requests.
  • The Remove → Reduce → Reuse ladder for slow code you can't fix: remove it, invoke it less, or memoize. Pick the right cache scope (
    @redis_cache
    ,
    @request_cache
    ,
    @site_cache
    — the last balloons memory if overused). DO NOT hand-roll caches in
    frappe.local
    or
    frappe.flags
    : "you'll just be creating brand-new cache-invalidation bugs." Don't cache trivially cheap work.
  • Memory: don't stuff junk into shared module-level files /
    __init__.py
    / class-level state — it stays resident forever. Remove unused module-level imports (move into the function that uses them). Watch for leaks.
  • Reorder conditionals so the DB call is last. In a boolean expression, put cheap in-memory checks first so short-circuiting can skip the query entirely.
  • Aggregate in SQL, not Python. Use
    SUM()
    /
    COUNT()
    in the query instead of fetching all rows to reduce them in memory; push filters into the subquery so they apply before the join.
  • Don't fetch a whole doc for one value. Use
    get_value
    /
    get_single_value
    /
    set_value
    for a single column instead of
    get_doc().save()
    ; use
    frappe.delete_doc
    instead of
    get_doc().delete()
    (which fetches the doc only to delete it).
  • No MyISAM tables in hot paths. Reading a MyISAM table takes an implicit table-level lock — never touch one in a request path.
  • Move long work to a background queue. Long-running work belongs in
    enqueue(..., queue="long")
    , not a synchronous request that blocks a worker.
修复性能的最佳时机是代码审查阶段;合并后的慢代码可能多年未被发现。性能是一项功能(Doherty阈值;人类对~100ms的延迟敏感)。
  • 性能预算:常规读取操作 < 100ms;读取操作 < 1s;大多数写入操作 < 5s;绝对不要超过~10s。频繁的只读请求的P99延迟应约为1s。缓慢的同步请求会阻塞工作进程(队头阻塞)。
  • 复杂度规则:频繁调用的端点必须执行O(1)或O(log N)的操作——最坏情况下是较大的常数因子,绝对不能是O(N)。统计行数是O(N),而非O(1);对大型/过滤后的表执行
    COUNT(*)
    代价高昂。限制无界扫描(例如仅扫描最近3个月的数据,或设置“1000+”的阈值)而非扫描全部数据。
  • 索引即代码。标记所有在未索引列上的
    WHERE
    /连接/过滤操作。索引(及自定义索引)必须在代码中提交,而非临时添加——否则在迁移时会丢失。加载表单时获取评论/版本/任务分配的所有查询都需要索引;一个未索引的查询会导致整个操作变慢。
  • 禁止在循环中调用数据库。“不要编写在循环中调用数据库的验证逻辑。”标记N+1查询模式。缓存稳定值(UOM、docstatus、status)而非重复查询。此规则在后台任务中可例外,但绝对不能在请求处理中出现。
  • 慢代码优化阶梯:移除→减少调用→复用:如果无法修复慢代码,先尝试移除它,减少调用次数,或进行记忆化缓存。选择正确的缓存范围(
    @redis_cache
    ,
    @request_cache
    ,
    @site_cache
    ——过度使用
    @site_cache
    会导致内存膨胀)。不要在
    frappe.local
    frappe.flags
    中手动实现缓存
    :“这只会带来全新的缓存失效bug”。不要缓存代价极低的操作。
  • 内存管理:不要将无用数据存入共享模块级文件 /
    __init__.py
    / 类级状态——这些数据会永久驻留内存。移除未使用的模块级导入(移至使用它的函数内部)。警惕内存泄漏。
  • 调整条件顺序,将数据库调用放在最后。在布尔表达式中,先执行廉价的内存内检查,这样短路求值可以跳过查询。
  • 在SQL中进行聚合,而非Python。在查询中使用
    SUM()
    /
    COUNT()
    而非获取所有行后在内存中聚合;将过滤条件推入子查询,使其在连接前生效。
  • 不要为了获取一个值而加载整个文档。使用
    get_value
    /
    get_single_value
    /
    set_value
    获取单个列的值,而非
    get_doc().save()
    ;使用
    frappe.delete_doc
    而非
    get_doc().delete()
    (后者会先加载文档再删除)。
  • 热路径中禁止使用MyISAM表。读取MyISAM表会隐式获取表级锁——绝对不要在请求处理路径中使用。
  • 将耗时操作移至后台队列。长时间运行的任务应放入
    enqueue(..., queue="long")
    ,而非阻塞工作进程的同步请求。

4. Concurrency

4. 并发

  • Check-then-act is a race.
    if not frappe.db.exists(...): insert()
    — two workers both see "not exists" and both insert. Prefer a DB-level unique constraint; "outsource integrity to the database."
  • Locking footguns:
    SELECT ... FOR UPDATE
    on an unindexed query locks every scanned row (and gaps) — always ensure the filter uses an index, or you lock the whole table. Locking a parent but not its children yields a "mutant" doc.
  • Global mutable state / class attributes are global in Python — a shared engine/class attribute leaking query state across concurrent requests produces garbage. Make query-building stateless. Don't do "weird shit with
    frappe.local
    " —
    local
    is for variables, not static state.
  • 检查后执行存在竞态条件
    if not frappe.db.exists(...): insert()
    ——两个工作进程可能都看到“不存在”并都执行插入。优先使用数据库级唯一约束;“将完整性保障交给数据库”。
  • 锁定陷阱:对未索引查询执行
    SELECT ... FOR UPDATE
    会锁定所有扫描到的行(及间隙)——始终确保过滤条件使用索引,否则会锁定整个表。锁定父文档但未锁定其子文档会产生“变异”文档。
  • 全局可变状态 / 类属性在Python中是全局的——共享引擎/类属性在并发请求间泄露查询状态会产生无效数据。查询构建必须无状态。不要对
    frappe.local
    进行“奇怪的操作”——
    local
    用于存储变量,而非静态状态。

5. Readability & maintainability

5. 可读性与可维护性

~50% of dev time is spent reading code; rotten code eventually forces a rewrite.
  • Keep functions pure when they can be pure — easy to read and test.
  • Don't pass mutable objects around to be filled in ("assembly" code) — return new values. Passing a mutable to be mutated forces a reader to open two files to understand one thing.
  • A function that mutates its input must be named appropriately.
  • Prefer the boring construct. While that functional map-reduce one-liner looks beautiful, please just write a 4-line for-loop. Favor debuggable code over clever code.
  • Consistency over personal style. A codebase shouldn't be a hodge-podge of 10 styles. Match the surrounding formatting/naming/import conventions; flag a change that breaks them.
  • Good taste: restructure so the edge case becomes the common case, removing special-case branches. Ask: can this be simpler? Less code? Is it over-indented if-else soup?
  • Prefer extending shared components over copy-paste divergence. 3–4 forked implementations of one thing → slow long-term velocity. Avoid tight coupling across modules; integrate through clear, documented public APIs.
  • Document public modules/classes/functions with docstrings; prefer type annotations over describing types in prose ("type hints are 10x better"); type checkers find non-obvious bugs.
  • Docstrings should only mention important things. Keep them short and to the point. Don't explain what's trivially understood from function name. Focus on "why".
  • Split unrelated changes into separate commits/PRs — keeps review focused and
    git blame
    /reverts clean.
约50%的开发时间用于阅读代码;糟糕的代码最终会迫使重构。
  • 尽可能保持函数纯函数化——易于阅读和测试。
  • 不要传递可变对象来填充数据(“组装式”代码)——返回新值。传递可变对象并修改它会迫使读者打开两个文件才能理解逻辑。
  • 修改输入的函数必须有合适的命名。
  • 优先使用常规结构。虽然函数式的map-reduce单行代码看起来很优雅,但请写一个4行的for循环。优先选择可调试的代码而非巧妙的代码。
  • 一致性优先于个人风格。代码库不应是10种风格的大杂烩。匹配周围代码的格式/命名/导入约定;标记破坏约定的变更。
  • 优化代码结构:重构代码使边缘情况变为常规情况,移除特殊分支。思考:能否更简单?代码能否更少?是否是过度缩进的if-else嵌套?
  • 优先扩展共享组件而非复制粘贴。同一功能有3-4个分叉实现会降低长期开发速度。避免模块间的紧耦合;通过清晰、有文档的公共API进行集成。
  • 公共模块/类/函数编写文档字符串;优先使用类型注解而非用文字描述类型(“类型提示比文字描述好10倍”);类型检查器能发现非显而易见的bug。
  • 文档字符串仅需提及重要内容。保持简短切题。不要解释从函数名就能轻易理解的内容。重点说明“为什么”。
  • 将无关变更拆分到不同的提交/PR中——使审查更聚焦,且
    git blame
    /回滚更清晰。

6. API design & backward compatibility

6. API设计与向后兼容性

  • Principle of least astonishment: an API's name + signature should convey ~90% of intent; users shouldn't be surprised by behavior.
  • Reject loose/overloaded parameters that accept many disjoint types (string/dict/list/None). Prefer separate single-purpose functions. Beware implicit fallbacks; use explicit variants. "APIs whose correct use depends on tribal knowledge are a liability."
  • Build for extension, not override. Provide hooks; never monkey-patch core at runtime ("inexcusably horrible" — breaks future fixes) and never copy a whole core file to change a few lines (fixes won't propagate).
  • Backward compatibility is an obligation for mature/public APIs. Follow semver; minor versions = zero breaking changes. Breaking changes include: removing public functions/fields, reordering args, new mandatory args, changed business logic, moved/renamed files (broken imports), bumped shared deps. Renaming without keeping the old name as an alias is an unnecessary break. Every breaking change ships a deprecation warning + docs.
  • Watch for schema breaking-change footguns: adding mandatory fields to existing sites, making long-lived fields unique (needs a data patch), changing field types without patches, removing fields.
  • Schema changes that silently skip existing sites need a data patch. Single doctypes don't sync new-field defaults to existing sites, and a field-type change (e.g. text→int) doesn't convert existing values — both need an explicit patch, tested against a populated site.
  • New parameters go last as keyword args with safe defaults (
    None
    , not
    ""
    ) so existing positional callers don't break. When renaming, keep the old name as a shim:
    def old_name(...): return new_name(...)
    .
  • Patch hygiene. Data patches must be idempotent (safe to re-run), correctly ordered (run after the field/doctype they read exists), and live in the right app (a framework change is patched in the framework, not the downstream app).
  • A modified existing test is a red flag. If making a change pass required editing an existing test's assertions, you've likely broken a real workflow — justify it explicitly rather than bending the test.
  • 最小惊讶原则:API的名称+签名应传达约90%的意图;用户不应对行为感到意外。
  • 拒绝松散/重载参数——即接受多种不相关类型(字符串/字典/列表/None)的参数。优先使用单独的单一用途函数。警惕隐式回退;使用显式变体。“正确使用依赖于内部知识的API是一种负担。”
  • 为扩展而构建,而非重写。提供钩子;绝对不要在运行时猴子补丁(monkey-patch)核心代码(“极其糟糕”——会破坏未来的修复),也不要复制整个核心文件来修改几行代码(修复不会同步)。
  • 向后兼容是成熟/公共API的义务。遵循语义化版本(semver);小版本=零破坏性变更。破坏性变更包括:移除公共函数/字段、调整参数顺序、新增必填参数、修改业务逻辑、移动/重命名文件(导致导入失败)、升级共享依赖。不保留旧名称作为别名的重命名属于不必要的破坏性变更。每一项破坏性变更都需附带弃用警告+文档说明。
  • 警惕架构破坏性变更陷阱:向现有站点添加必填字段、将长期存在的字段设为唯一(需要数据补丁)、无补丁修改字段类型、移除字段。
  • 静默跳过现有站点的架构变更需要数据补丁。单个文档类型不会将新字段的默认值同步到现有站点,字段类型变更(例如文本→整数)不会转换现有值——这两种情况都需要显式补丁,并针对有数据的站点进行测试。
  • 新增参数应放在最后作为带安全默认值的关键字参数
    None
    而非
    ""
    ),这样现有的位置参数调用不会失效。重命名时,保留旧名称作为垫片:
    def old_name(...): return new_name(...)
  • 补丁规范。数据补丁必须幂等(可安全重复运行)、顺序正确(在其读取的字段/文档类型存在后运行),且位于正确的应用中(框架变更应在框架中打补丁,而非下游应用)。
  • 修改现有测试是危险信号。如果为了让变更通过而修改现有测试的断言,很可能破坏了实际工作流程——需明确说明理由,而非修改测试来适配变更。

7. Testing

7. 测试

  • Each PR needs decent test coverage — patch coverage on the diff, not just project coverage. (Frappe target: 85% covered lines in the diff.) Tests should capture the most-used business scenarios.
  • Regression test every fix. A bug fix without a test that would have caught it invites the regression back. For extreme-consequence (stateful/compliance) code, go beyond examples — property-based testing (Hypothesis).
  • Flag missing migration/data-patch coverage. Schema changes and data patches are the highest-risk, least-tested area; a change that alters fields or migrates data needs a patch tested against a realistic, populated site (empty tables always "migrate" successfully even when the change is invalid).
  • Tests must be deterministic and independent. No
    random
    (flaky); no reliance on state left by other tests (order-dependence); use
    freeze_time
    for time-dependent logic.
  • 每个PR都需要足够的测试覆盖率——针对差异的补丁覆盖率,而非仅项目整体覆盖率。(Frappe目标:差异中85%的代码行被覆盖。)测试应覆盖最常用的业务场景。
  • 每一个修复都需要回归测试。没有测试的bug修复会导致问题再次出现。对于高风险(状态/合规)代码,除示例测试外,还应使用属性测试(Hypothesis)。
  • 标记缺失的迁移/数据补丁测试覆盖。架构变更和数据补丁是风险最高、测试最少的领域;修改字段或迁移数据的变更需要针对真实的、有数据的站点测试补丁(空表总是能“成功迁移”,即使变更无效)。
  • 测试必须是确定性且独立的。不要使用
    random
    (会导致测试不稳定);不要依赖其他测试留下的状态(顺序依赖);对时间相关逻辑使用
    freeze_time

8. Error messages, logging & observability

8. 错误信息、日志与可观测性

  • Error message quality is a legitimate review item. Titles must be specific and Google-able (never "Message"/"Error"). Reference field names as fields. State what changed: the row, the field, and before→after values (
    1 → 2
    ). The user must know "qty changed from what to what?"
  • Surface failures to the affected party — "a broken email setup is the user's problem only if they know it's broken."
  • Log things. Preserve tracebacks/exception context (orders of magnitude easier debugging). Log destructive/admin actions with attributable identity (who, when, from where), persisted outside ephemeral containers.
  • 错误信息的质量是合理的审查项。标题必须具体且可搜索(绝对不要用“Message”/“Error”)。引用字段名称。说明变更内容:行、字段、前后值(
    1 → 2
    )。用户必须知道“数量从什么值变成了什么值?”
  • 向受影响的用户告知故障——“邮件配置故障只有在用户知道的情况下才是他们的问题。”
  • 记录日志。保留回溯/异常上下文(大幅简化调试)。记录破坏性/管理操作的可追溯身份(谁、何时、从何处操作),并存储在临时容器之外。",