domains / app-frameworks / honojs/Hono
Write effective, deterministic tests
Every test must genuinely validate the behavior it claims to cover, run deterministically, and avoid noise from redundant or stale cases. - **Verify the test actually fails without the fix.** Before adding a regression test, temporarily revert the fix and confirm the test fails. A test that passes on both old and new code gives false confidence (e.g....
Every test must genuinely validate the behavior it claims to cover, run deterministically, and avoid noise from redundant or stale cases.
- Verify the test actually fails without the fix. Before adding a regression test, temporarily revert the fix and confirm the test fails. A test that passes on both old and new code gives false confidence (e.g. asserting
cookies.lengthinstead of the actual cookie values, or asserting generic type inequality instead of the concrete expected type). - Assert concrete expected values, not just shape or count. Prefer
expect(cookies).toContain('foo=bar; Path=/')overexpect(cookies.length).toBe(2), and prefertoBeInstanceOf(ALBProcessor)over a loose truthy check. - Keep tests deterministic and isolated. Never use
Math.random(), real network ports, or timing-dependent assumptions in tests — use fixed/mocked values (e.g. mock HTTP servers withmswinstead of spinning up a real server on a fixed port, which can collide with other processes), and make sure async event listeners resolve on the correct event (e.g. resolve onclose, notopen, when testing a close handshake). - Avoid redundant or misplaced tests. Don’t add a test that duplicates existing coverage (e.g. a
::case already covered by a trailing-::test), and don’t add tests to a module’s suite that belong elsewhere (e.g. generic request-cloning tests insidevalidator.test.ts). Do not remove existing tests without a replacement unless the behavior they cover is actually gone. - Cover meaningful edge cases, including negatives and defaults. When adding a feature, also test the “off”/default case (e.g. verify no
report-uriis emitted when unspecified) and boundary inputs (empty input, single-element arrays, trailing slashes, etc.). - Clean up test artifacts before merging. Remove stray
.only()/.skip()calls, debugconsole.log/console.debugstatements, and unused scaffolding. Ifanyis intentionally used in a test, suppress the lint warning explicitly (// eslint-disable-next-line @typescript-eslint/no-explicit-any) rather than leaving a warning.
Example of a weak vs. strong test:
// Weak: passes even with the bug, and only checks length
it('keeps previous cookies', () => {
const res = c.json({ message: 'Hello' })
expect(res.headers.getSetCookie().length).toBe(2)
})
// Strong: verifies actual values, would fail without the fix
it('keeps previous cookies', () => {
const res = c.json({ message: 'Hello' })
const cookies = res.headers.getSetCookie()
expect(cookies).toContain('foo=bar; Path=/')
expect(cookies).toContain('foo2=bar2; Path=/')
})