denoland/deno · error · Error

plan expected ${this.#expected} assertion(s) but received ${

Error message

plan expected ${this.#expected} assertion(s) but received ${this.#actual}

What it means

t.plan(count) in node:test registers a TestPlan with the expected number of assertions. The test context wraps its assert object so every t.assert.* call (and t.assert.fileSnapshot) increments a counter, and when the body finishes _checkPlan() compares actual against expected. Any mismatch — too few or too many — throws 'plan expected N assertion(s) but received M'.

Source

Thrown at ext/node/polyfills/testing.ts:1353

  }
  return undefined;
}

class TestPlan {
  #expected;
  #actual = 0;

  constructor(count) {
    this.#expected = count;
  }

  increment() {
    this.#actual++;
  }

  check() {
    if (this.#actual !== this.#expected) {
      throw new Error(
        `plan expected ${this.#expected} assertion(s) but received ${this.#actual}`,
      );
    }
  }
}

class NodeTestContext {
  #denoContext;
  #afterHooks = [];
  #beforeHooks = [];
  #parent;
  #skipped = false;
  #name;
  #abortController = new AbortController();
  #plan;
  #planAssert;
  #beforeEachHooks = [];
  #afterEachHooks = [];

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Make every planned assertion unconditional — move variability into assertion arguments, not around the assert calls
  2. Recount after editing: add/remove assertions and update t.plan(n) in the same change
  3. Verify you are calling t.assert.* (the context's wrapped object), not the imported node:assert — only the former is counted
  4. Derive the count from data when looping: t.plan(cases.length) then one assert per case
  5. If the exact count is unknowable, drop t.plan entirely — it is optional

Example fix

// before
test('sum', (t) => {
  t.plan(3);
  t.assert.equal(1 + 1, 2);
  if (process.env.EXTRA) t.assert.ok(true); // skipped -> received 2
});

// after
test('sum', (t) => {
  t.plan(2);
  t.assert.equal(1 + 1, 2);
  t.assert.ok(true);
});
Defensive patterns

Strategy: validation

Validate before calling

// dev-time dry run: count t.assert usage to derive the plan
function planFromBody(t, body) {
  let count = 0;
  const assert = new Proxy(t.assert, {
    get(target, prop) {
      const v = Reflect.get(target, prop);
      return typeof v === 'function'
        ? (...args) => { count++; return v(...args); }
        : v;
    },
  });
  return { run: body({ ...t, assert }), count: () => count };
}
// use once locally, then hard-code t.plan(count) in the test

Prevention

When it happens

Trigger: t.plan(3) with only two assertions executed because an early return or if-branch skipped one; asserting more times than planned inside a loop; using the imported node:assert functions instead of t.assert.* so calls are never counted.

Common situations: Conditional assertions (if (debug) t.assert.ok(...)); loop-driven asserts whose iteration count varies with data; refactors that swap t.assert for the global assert module; forgetting that plan counts only this test's own asserts, not subtests.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/82231297ba643658. Report an issue: GitHub.