principle-test-behavior-not-implementation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test Behavior, Not Implementation

测试行为,而非实现

A test calls the code the way its users do and asserts the result they observe against a literal expected value. A test that asserts which calls the code made, or restates a constant the code contains, does neither.
The check: before you keep a test, ask whether it would still pass if every function it imports returned
undefined
. If yes, it observes no behavior and cannot fail for a defect. Rewrite the assertion or delete the test.
Why: A test that cannot fail for a defect costs CI time and review attention and catches nothing. A constant pin also fails when someone edits the constant or the prompt it restates, so it prevents that edit.
Five shapes that still pass when every imported function returns
undefined
:
  • Weak or no assertion. No
    expect
    , or only
    toBeDefined
    ,
    toBeTruthy
    ,
    not.toThrow
    ,
    toBeInstanceOf
    ,
    toBeGreaterThan(0)
    .
  • Mock or absence only. Only
    toHaveBeenCalled
    ,
    not.toHaveBeenCalled
    ,
    toBeUndefined
    ,
    toEqual([])
    ,
    toHaveLength(0)
    ,
    not.toBe(wrongValue)
    .
  • Self-referential. The expected value comes from the code under test:
    expect(f(a)).toBe(f(a))
    ,
    expect(parsed.url).toBe(buildUrl(...))
    .
  • Constant pin. The assertion restates a hand-maintained constant, config default, table row, or prompt string:
    expect(LIMITS.maxTools).toBe(8)
    ,
    expect(PROMPT).toContain("You are")
    .
  • Fixture asserts fixture. The assertion reads data the test built or a value computed in
    beforeEach
    , and the subject never runs inside the body.
The fix: call the subject inside the test body with one concrete input and assert the literal output or the observable effect,
expect(slugify("Hello, World!")).toBe("hello-world")
. For an absence, assert the presence on the other input in the same test. For a constant, test the mechanism that reads it with one input instead of restating the value. For a mock, assert the payload it received or the state after the call, not that it was called. When no such assertion exists, delete the test.
Keep a test of a relation across a table's rows (a key present in two tables, a parent that exists), and a compile-time check in a
*.test-d.ts
file.
测试应像用户一样调用代码,并将观察到的结果与字面量预期值进行断言。断言代码进行了哪些调用,或只是重述代码中包含的常量的测试,两者都做不到这一点。
检查方法:在保留测试前,问问自己:如果测试导入的每个函数都返回
undefined
,该测试是否仍会通过?如果是,它就没有观察到任何行为,也无法因缺陷而失败。请重写断言或删除该测试。
原因: 无法因缺陷而失败的测试会消耗 CI 时间和评审注意力,却什么也捕获不到。固定常量的断言还会在有人编辑该常量或其重述的提示词时失败,因此会阻碍这类编辑。
以下五种形式在所有导入函数都返回
undefined
时仍会通过:
  • 断言薄弱或不存在。 没有
    expect
    ,或者只使用
    toBeDefined
    、
    toBeTruthy
    、
    not.toThrow
    、
    toBeInstanceOf
    、
    toBeGreaterThan(0)
    。
  • 仅验证 Mock 或不存在状态。 只使用
    toHaveBeenCalled
    、
    not.toHaveBeenCalled
    、
    toBeUndefined
    、
    toEqual([])
    、
    toHaveLength(0)
    、
    not.toBe(wrongValue)
    。
  • 自引用。 预期值来自被测代码:
    expect(f(a)).toBe(f(a))
    、
    expect(parsed.url).toBe(buildUrl(...))
    。
  • 固定常量的断言。 断言重述了手动维护的常量、配置默认值、表格行或提示词字符串:
    expect(LIMITS.maxTools).toBe(8)
    、
    expect(PROMPT).toContain("You are")
    。
  • Fixture 断言 Fixture。 断言读取的是测试自身构建的数据,或
    beforeEach
    中计算出的值,且被测对象在测试体内从未运行。
修复方法: 在测试体内用一个具体输入调用被测对象,并断言字面量输出或可观察效果,例如
expect(slugify("Hello, World!")).toBe("hello-world")
。如果要验证某项不存在,请在同一个测试中用另一个输入断言其存在。对于常量,应使用一个输入测试读取该常量的机制,而不是重述其值。对于 Mock,应断言其接收到的载荷或调用后的状态,而不是断言它是否被调用。如果不存在这样的断言,请删除该测试。
保留 跨表格各行关系的测试(例如,一个键存在于两个表中、某个父项存在),以及
*.test-d.ts
文件中的编译时检查。