Skip to content

Module mocking ​

deride creates mock objects; your test runner handles substituting them at import time. Use the two together.

Vitest ​

typescript
import { describe, it, vi } from 'vitest'
import { stub } from 'deride'
import type { Database } from './database'

// Create the mock up front
const mockDb = stub<Database>(['query', 'findById'])
mockDb.setup.query.toResolveWith([{ id: 1 }])

// Substitute the import
vi.mock('./database', () => ({
  db: mockDb,
}))

// Code under test imports './database' and uses the mock
import { userService } from './user-service'

describe('userService', () => {
  it('queries the database', async () => {
    await userService.listUsers()
    mockDb.expect.query.called.once()
  })
})

Vitest hoists vi.mock to the top of the file, so it runs before import { userService } resolves the module graph.

Jest ​

typescript
import { stub } from 'deride'
import type { Database } from './database'

const mockDb = stub<Database>(['query'])

jest.mock('./database', () => ({
  db: mockDb,
}))

import { userService } from './user-service'

Same story — jest.mock is hoisted. If you're using jest ESM, use jest.unstable_mockModule instead and dynamic-import the module under test.

Node node:test test runner ​

typescript
import { describe, it, mock } from 'node:test'
import { stub } from 'deride'
import type { Database } from './database'

const mockDb = stub<Database>(['query'])
mock.module('./database', () => ({
  db: mockDb,
}))

const { userService } = await import('./user-service')

Node's mock.module is ESM-friendly but not hoisted — use a dynamic import for the module under test.

When hoisting doesn't play nicely ​

Some setups (e.g. tests that construct new URL() during static analysis, or tests where the mock depends on data from the test) can't hoist vi.mock/jest.mock nicely. Two options:

Inject the dependency instead ​

The cleanest fix is almost always to restructure the code under test to accept the dependency via constructor or factory:

typescript
// user-service.ts
export class UserService {
  constructor(private db: Database) {}
  async listUsers() { return this.db.query(...) }
}

// user-service.test.ts — no vi.mock needed
const mockDb = stub<Database>(['query'])
const service = new UserService(mockDb)

This is deride's preferred style. It makes your tests shorter, clearer, and runner-agnostic.

Dynamic factories ​

For cases where injection isn't an option, use the runner's module-factory hooks:

typescript
// vitest — the factory runs once, lazily
vi.mock('./database', async () => {
  const { stub } = await import('deride')
  return { db: stub<Database>(['query']) }
})

Referencing the mock after this returns something subtly different — use vi.mocked(...) to get a typed handle.

Partial module mocks ​

Sometimes you want to mock some exports from a module and keep others real:

typescript
// vitest
vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./utils')>()
  return {
    ...actual,
    parseConfig: stub<typeof actual.parseConfig>(['parseConfig']).parseConfig,
  }
})

For the mocked function specifically, wrap is usually simpler — wrap the real function and override only when needed:

typescript
const wrappedParseConfig = wrap(actual.parseConfig)
wrappedParseConfig.setup.toReturn({ fake: 'config' })

Typescript + mock type preservation ​

typescript
// vitest
import { vi } from 'vitest'
import { stub, type Wrapped } from 'deride'
import { db } from './database'

vi.mock('./database', () => ({
  db: stub<typeof db>(['query']),
}))

// Help TypeScript see the type:
const mockDb = vi.mocked(db) as unknown as Wrapped<typeof db>
mockDb.setup.query.toResolveWith([])

When to avoid module mocking ​

  • When you can inject the dependency. Constructor/parameter injection means your tests don't need any runner-specific mocking at all. deride works best with this style.
  • For pure utility modules. If the module is side-effect-free and fast, just call it. Mocking pure computation is usually a smell.

See also ​

Released under the MIT License.