Unknown Test.
A test method that exercises the code but contains no assertion, so it passes as long as nothing throws — leaving its actual purpose and what it verifies unknown.
##Signs and Symptoms
A test that sets up objects and calls the system under test, then stops without a single assertion. It is green simply because no exception was thrown — not because any expected behavior was confirmed. Nothing in the body states what "correct" looks like.
Tell-tale signs:
- No
expect/assert/verifyanywhere in the test body. - "Verification" done by
console.log/printof the result that a human is supposed to eyeball. - A vague test name (
testChainDependencies) and a body that gives no hint of what it guarantees. - The test would stay green even if the production method returned a completely wrong value.
- Tests that lean entirely on "it didn't throw" without saying so explicitly.
// Smell: runs code, prints, asserts nothing — passes no matter what calculate() returns
test('chain dependencies', () => {
const game = Game.newGame(0, '');
game.setOtherGoods(Building.TOOLMAKERS, 1);
const logic = new Logic(game);
const res = logic.calculateChainWithDependencies(Goods.TOOLS);
console.log(res.toString()); // no expect(...) — what is this checking?
});
A common variant hides behind heavy mocking: the test wires up mocks and calls the SUT but never asserts on a return value or on the mock interactions.
##Reasons for the Problem
Why it happens
- A placeholder/TODO test was scaffolded ("make it compile and run") and the assertion was never filled in.
- Debugging scaffolding —
console.log, a manual sanity run — got committed as if it were a test. - A refactor deleted or commented out the assertion but left the setup behind.
- "It runs without an exception" is mistaken for "it works." Smoke tests are legitimate, but here the intent to smoke-test is never made explicit.
- Auto-generated or AI-generated test stubs that exercise a method without checking the outcome.
Why it hurts
- False confidence. The test contributes to the green bar and to line/branch coverage, yet verifies nothing. Coverage metrics actively lie about how protected the code is.
- No regression protection. Behavior can silently break — wrong return value, wrong state — and the suite stays green. This is the worst kind of test: it costs maintenance but catches nothing.
- Unknown intent. A reader (or future maintainer) cannot tell which behavior is guaranteed, so they can't safely change the code or the test. It documents nothing.
- Erodes trust in the suite. Once people notice tests that don't really test, they stop believing green means good — undermining every other test too.
It is the mirror image of Assertion Roulette: that smell has too many undocumented assertions; Unknown Test has none.
##Treatment
Make every test state what it expects, and make "it just shouldn't throw" an explicit, deliberate choice.
- Add at least one assertion on an observable outcome — the return value, resulting state, or a thrown error. Replace
console.log/print debugging with anexpecton that value. - If the real intent is "this must not throw," say so explicitly with
expect(() => fn()).not.toThrow()(orawait expect(fn()).resolves.toBeDefined()). Now the smoke test is documented rather than accidental. - For interaction-only tests, assert on the collaborator:
expect(mock).toHaveBeenCalledWith(...). - Delete or
skip/tododead placeholders instead of leaving an empty green test (it.todo('handles chained deps')records the gap honestly without faking coverage). - Turn on a detector (
jest/expect-expect,vitest/expect-expect, or SonarSource S2699) in CI. If you wrap assertions in custom helpers, register them via the rule'sassertFunctionNamesoption so genuine assertions aren't flagged.
// Before — Unknown Test
test('chain dependencies', () => {
const logic = new Logic(Game.newGame(0, ''));
const res = logic.calculateChainWithDependencies(Goods.TOOLS);
console.log(res.toString());
});
// After — intent and guarantee are explicit
test('resolves tools to the toolmakers workshop chain', () => {
const logic = new Logic(Game.newGame(0, ''));
const res = logic.calculateChainWithDependencies(Goods.TOOLS);
expect(res).toHaveLength(1);
expect(res[0].building).toBe(Building.TOOLMAKERS);
});
##Detected by
- eslint-jest expect-expect — jest/expect-expect
- eslint-vitest expect-expect — vitest/expect-expect
- sonar S2699 — Tests should include assertions (JavaScript)
- sonar S2699 — Tests should include assertions (Java)