Ask
138
@sawdust_pete ·

vi.mock factory throws cannot access mockDb before initialization Mocking

const mockDb = { query: vi.fn() }
vi.mock('~/db', () => ({ db: mockDb }))

Every test in the file dies with ReferenceError: Cannot access 'mockDb' before initialization.

The part I cannot get my head around: the const already is above the mock. Moving it further up changes nothing. Vitest 3, TS, ~ alias resolved through the vite tsconfig paths plugin.

7 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @kombucha_kai · 2mo ago · 2 replies

    vi.mock calls are hoisted to the top of the module by the transform, above everything you wrote, regardless of where they appear in the file. That is deliberate - the mock has to be registered before the imports it replaces are evaluated. So the factory runs first and mockDb is still in its temporal dead zone.

    Hoist the value with the mock:

    const { mockDb } = vi.hoisted(() => ({
      mockDb: { query: vi.fn() },
    }))
    
    vi.mock('~/db', () => ({ db: mockDb }))
    

    vi.hoisted gets lifted alongside the mock, so both the factory and your tests can see the same object. Rule of thumb: anything the factory touches must be created inside vi.hoisted.

    171
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @thriftedtweed · 2mo ago

      And for resetting between tests, call mockDb.query.mockReset() yourself in beforeEach. The clearMocks config option clears call history but leaves implementations in place, which produces a very confusing second test.

      43
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @blown_cap · 2mo ago · 2 replies

    Unpopular: do not mock the database. Use an actual Postgres - a container, or one of the in-process builds - with your real schema and a transaction or a fresh schema per test.

    A mocked query builder means your tests pass while your SQL is wrong, and wrong SQL is the only thing database code can actually get wrong. You are mocking out the part under test to make the part you already trust easier to arrange.

    63
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @row_level_sam · 2mo ago

      Strongly agree, and if you use row level security it stops being a preference. A mocked client cannot tell you a policy is missing. It will happily return the rows you told it to return, including the ones your policy should have hidden.

      29
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @nightbus_nina · 2mo ago

    Alternative when different tests in one file need different mock shapes: vi.doMock is not hoisted, so plain consts work normally. The cost is that the module under test then has to be imported inside the test with await import('...') rather than at the top of the file, because the mock has to be registered first.

    Slightly uglier, much easier to reason about when the mock is per-test.

    88
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @wick_and_wax · 2mo ago

    One more trap once the hoisting is fixed: the specifier you mock has to resolve to the same module id as the import inside the code under test. If one file imports ~/db and another imports ../db, you can end up with the mock applied to one of them and not the other, and the failure looks like "the mock did nothing".

    When a mock mysteriously does not apply, log the resolved id on both sides before you start rewriting the test.

    34
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @compost_turner · 2mo ago

    Or pass the db in as an argument. Modules that take their dependencies never need any of this machinery, and the test reads as three lines instead of a hoisting lecture.

    11
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report