A TypeScript-first mocking library that wraps rather than monkey-patches.
A TypeScript-first mocking library that works with frozen objects, sealed classes, and any coding style — no monkey-patching required.
Why deride?
- Works with
Object.freeze, ES6 classes, and prototype-based objects - Composition-based — wraps objects rather than mutating them
- Framework-agnostic — works with vitest, jest, node:test, or anything that catches thrown errors
- Type-safe — setup methods are constrained to your method signatures
- Zero config — no decorators, no DI container, no babel plugins
- Sub-path integrations (
deride/vitest,deride/jest,deride/clock) are opt-in, so the core stays small
import { stub } from 'deride'
const mockDb = stub<Database>(['query', 'findById'])
mockDb.setup.query.toResolveWith([{ id: 1, name: 'alice' }])
const result = await mockDb.query('SELECT * FROM users')
mockDb.expect.query.called.once()
mockDb.expect.query.called.withArg('SELECT * FROM users')npm install derideimport deride from 'deride'
// or use named exports
import { stub, wrap, func, match, inOrder, sandbox } from 'deride'deride.stub(methods)— create a stub from method namesderide.stub(obj)— create a stub from an existing objectderide.stub(Class)— create a stub from a class (auto-discovers prototype methods)deride.stub.class<typeof C>()— mock a constructor, tracknewcalls, produce per-instance stubsderide.wrap(obj)— wrap an existing object (works with frozen objects)deride.wrap(fn)— wrap a standalone functionderide.func(original?)— create a standalone mocked functionderide.sandbox()— create a scope with fan-out reset/restore
Return-value behaviours:
setup.method.toReturn(value)setup.method.toReturnSelf()setup.method.toReturnInOrder(...values)— sequential returnssetup.method.toDoThis(fn)setup.method.toThrow(message)
Async behaviours:
setup.method.toResolveWith(value)setup.method.toResolve()setup.method.toRejectWith(error)setup.method.toResolveInOrder(...values)setup.method.toRejectInOrder(...errors)setup.method.toResolveAfter(ms, value)— delayed resolvesetup.method.toRejectAfter(ms, error)setup.method.toHang()— never settles
Iterator behaviours:
setup.method.toYield(...values)setup.method.toAsyncYield(...values)setup.method.toAsyncYieldThrow(error, ...valuesBefore)
Callbacks, events, and interception:
setup.method.toCallbackWith(...args)setup.method.toEmit(event, ...args)setup.method.toIntercept(fn)setup.method.toTimeWarp(ms)
Routing and lifecycle:
setup.method.when(value | matcher | predicate)setup.method.once()/twice()/times(n)setup.method.toReturn(...).twice().and.then.toReturn(...)setup.method.fallback()
match.*namespace — reusable matchers forwhen()andexpect.*
Count and argument assertions:
expect.method.called.times(n)/once()/twice()/never()expect.method.called.lt(n)/lte(n)/gt(n)/gte(n)expect.method.called.withArg(arg)expect.method.called.withArgs(...args)expect.method.called.withMatch(regex)expect.method.called.matchExactly(...args)
Return / this / throw assertions:
expect.method.called.withReturn(expected)expect.method.called.calledOn(target)expect.method.called.threw(expected?)
Every-call / invocation / negation:
expect.method.everyCall.*expect.method.invocation(i).withArg(arg)expect.method.not.called.*— negated versionsexpect.method.called.reset()obj.called.reset()— reset all methods
mock.spy.method.callCount/calls/firstCall/lastCallmock.spy.method.calledWith(...)mock.spy.method.printHistory()mock.spy.method.serialize()— snapshot-friendly output
inOrder(...spies)— cross-mock call ordering
obj.snapshot()/obj.restore(snap)— per-mock state capturesandbox().reset()/.restore()— fan-out across registered mocks
deride/vitest— toHaveBeenCalled-style matchersderide/jest— same matchers for jestderide/clock— lightweight fake timers
The examples below use this interface:
interface Person {
greet(name: string): string
echo(value: string): string
}const bob = deride.stub<Person>(['greet', 'echo'])
bob.setup.greet.toReturn('hello')
bob.greet('alice') // 'hello'
bob.expect.greet.called.once()const bob = deride.stub<Person & { age: number }>(
['greet'],
[{ name: 'age', options: { value: 25, enumerable: true } }]
)
bob.age // 25const realPerson: Person = { greet: (n) => `hi ${n}`, echo: (v) => v }
const bob = deride.stub(realPerson)
bob.greet('alice')
bob.expect.greet.called.once()Passing a constructor auto-discovers prototype methods (walking inheritance), skipping constructor and accessor properties:
class Greeter {
greet(name: string) { return `hi ${name}` }
shout(name: string) { return `HI ${name.toUpperCase()}` }
static version() { return '1.0' }
}
const mock = deride.stub(Greeter)
mock.setup.greet.toReturn('mocked')
mock.greet('x') // 'mocked'
// Include static methods instead of prototype methods:
const staticMock = deride.stub(Greeter, undefined, {
debug: { prefix: 'deride', suffix: 'stub' },
static: true,
})
staticMock.setup.version.toReturn('mocked-v')Track new invocations AND provide fresh per-instance stubs:
const MockedDb = deride.stub.class(Database)
const a = new MockedDb('connection-string')
a.setup.query.toResolveWith([{ id: 1 }])
MockedDb.expect.constructor.called.once()
MockedDb.expect.constructor.called.withArg('connection-string')
// setupAll applies to every instance (past and future):
MockedDb.setupAll((inst) => inst.setup.query.toResolveWith([]))
// List all constructed stubs:
MockedDb.instances // [a, ...]Works with Object.freeze, ES6 classes, and prototype-based objects:
const person = Object.freeze({
greet(name: string) { return `hello ${name}` },
})
const bob = deride.wrap(person)
bob.greet('alice') // 'hello alice'
bob.expect.greet.called.withArg('alice')function greet(name: string) { return `hello ${name}` }
const wrapped = deride.wrap(greet)
wrapped('world') // 'hello world'
wrapped.expect.called.withArg('world')
wrapped.setup.toReturn('overridden')
wrapped('x') // 'overridden'// Empty mock
const fn = deride.func()
fn.setup.toReturn(42)
fn('hello') // 42
fn.expect.called.withArg('hello')
// Wrapping an existing function
const fn2 = deride.func((x: number) => x * 2)
fn2.setup.toReturn(99)
fn2(5) // 99Group mocks together for bulk cleanup:
import { sandbox } from 'deride'
const sb = sandbox()
const mockDb = sb.stub<Database>(['query'])
const mockLog = sb.wrap(realLogger)
afterEach(() => sb.reset()) // clear call history on all registered mocks
afterAll(() => sb.restore()) // clear behaviours and historyMocks created outside the sandbox are untouched. Nested sandboxes are independent.
bob.setup.greet.toReturn('foobar')
bob.greet('alice') // 'foobar'Useful for fluent/chainable APIs (query builders, jQuery-style):
const q = deride.stub<QueryBuilder>(['where', 'orderBy', 'execute'])
q.setup.where.toReturnSelf()
q.setup.orderBy.toReturnSelf()
q.setup.execute.toReturn([])
q.where('x').orderBy('y').where('z').execute()Sequential returns. The last value is sticky:
bob.setup.greet.toReturnInOrder('first', 'second', 'third')
bob.greet() // 'first'
bob.greet() // 'second'
bob.greet() // 'third'
bob.greet() // 'third' (sticky-last)
// With explicit fallthrough:
bob.setup.greet.toReturnInOrder(['a', 'b'], { then: 'default' })
// With cycling:
bob.setup.greet.toReturnInOrder(['a', 'b'], { cycle: true })
// a, b, a, b, a, ...bob.setup.greet.toDoThis((name) => `yo ${name}`)
bob.greet('alice') // 'yo alice'bob.setup.greet.toThrow('BANG')
bob.greet('alice') // throws Error('BANG')bob.setup.greet.toResolveWith('async result')
await bob.greet('alice') // 'async result'Resolve with undefined:
bob.setup.save.toResolve()
await bob.save(data) // undefinedbob.setup.greet.toRejectWith(new Error('network error'))
await bob.greet('alice') // rejectsbob.setup.fetch.toResolveInOrder('first', 'second', 'third')
await bob.fetch() // 'first'
await bob.fetch() // 'second'bob.setup.fetch.toRejectInOrder(new Error('a'), new Error('b'))Delay the resolution — pairs with fake timers (see deride/clock or vi.useFakeTimers()):
bob.setup.fetch.toResolveAfter(100, { data: 42 })
const p = bob.fetch('/x') // pending
// ...advance time by 100ms...
await p // { data: 42 }bob.setup.fetch.toRejectAfter(100, new Error('timeout'))Never-settling promise — ideal for exercising timeout paths:
bob.setup.fetch.toHang()
const p = bob.fetch('/x')
// p never resolves. Use Promise.race with a timeout to verify your code handles it.Return a fresh sync iterator:
bob.setup.stream.toYield(1, 2, 3)
for (const v of bob.stream()) console.log(v) // 1, 2, 3bob.setup.streamAsync.toAsyncYield(1, 2, 3)
for await (const v of bob.streamAsync()) console.log(v) // 1, 2, 3bob.setup.streamAsync.toAsyncYieldThrow(new Error('drained'), 1, 2)
// yields 1, 2, then throwsFinds the last function argument and invokes it with the provided args:
bob.setup.load.toCallbackWith(null, 'data')
bob.load('file.txt', (err, data) => { /* err=null, data='data' */ })bob.setup.greet.toEmit('greeted', 'payload')
bob.on('greeted', (data) => { /* data === 'payload' */ })
bob.greet('alice')Calls the interceptor with the arguments, then calls the original method:
const log: any[] = []
bob.setup.greet.toIntercept((...args) => log.push(args))
bob.greet('alice') // original runs, returns normal resultAccelerates the timeout — schedules the callback with the given delay instead of the original:
bob.setup.foobar.toTimeWarp(0) // immediate callback
bob.foobar(10000, (result) => { /* called immediately */ })Apply behavior conditionally:
// Match by value (first argument compared with deep equal)
bob.setup.greet.when('alice').toReturn('hi alice')
// Match by predicate
bob.setup.greet.when((args) => args[0].startsWith('Dr')).toReturn('hello doctor')
// Match by matcher
import { match } from 'deride'
bob.setup.greet.when(match.string).toReturn('hello string')
bob.setup.greet.when(match.objectContaining({ id: 1 })).toReturn('found')Limit how many times a behavior applies:
bob.setup.greet.once().toReturn('first')
bob.setup.greet.toReturn('default')
bob.greet() // 'first'
bob.greet() // 'default'Dispatch rule: within the time-limited behaviours, first-registered wins (FIFO). After those exhaust, the last unlimited behaviour wins. Use once() / times() when you need a conditional behaviour to beat a later default.
bob.setup.greet
.toReturn('alice')
.twice()
.and.then
.toReturn('sally')
bob.greet() // 'alice'
bob.greet() // 'alice'
bob.greet() // 'sally'Clear all configured behaviors, revert to the original implementation (or undefined for pure stubs).
bob.setup.greet.toReturn('mocked')
bob.greet('x') // 'mocked'
bob.setup.greet.fallback()
bob.greet('x') // original valueComposable, brand-tagged matchers that work in setup.when(), expect.*.withArg(), expect.*.withArgs(), expect.*.matchExactly(), expect.*.invocation(i).withArg(), withReturn(), threw(), and inside nested objects.
import { match } from 'deride'
// Type matchers
match.any / match.defined / match.nullish
match.string / match.number / match.boolean / match.bigint / match.symbol
match.array / match.object / match.function
// Structure
match.instanceOf(Ctor)
match.objectContaining({ id: 1, tag: match.string })
match.arrayContaining([1, match.number])
match.exact({ a: 1 }) // strict deep equal, rejects extra keys
// Comparators
match.gt(n) / match.gte(n) / match.lt(n) / match.lte(n)
match.between(low, high) // inclusive
// Strings
match.regex(/pattern/)
match.startsWith('pre') / match.endsWith('post') / match.includes('mid')
// Logic
match.not(m)
match.allOf(a, b, c) // every matcher must pass
match.oneOf(a, b) // at least one
match.anyOf(v1, v2, v3) // equality OR matcher match against each
// Escape hatch
match.where((v) => /* custom predicate */)Usage examples:
// In setup — conditional dispatch
mock.setup.fetch.when(match.string).toResolveWith('str')
mock.setup.fetch.when(match.objectContaining({ id: 1 })).toResolveWith(payload)
// In expectations — verification
mock.expect.fetch.called.withArg(match.string)
mock.expect.fetch.called.withArgs(match.string, match.number)
mock.expect.fetch.called.matchExactly(match.any, match.instanceOf(Error))
// In nested structures
mock.expect.save.called.withArg({ id: match.number, name: match.string })bob.expect.greet.called.times(2)
bob.expect.greet.called.once()
bob.expect.greet.called.twice()
bob.expect.greet.called.never()bob.expect.greet.called.gt(2)
bob.expect.greet.called.gte(3)
bob.expect.greet.called.lt(4)
bob.expect.greet.called.lte(3)Partial deep match across all recorded calls (matcher-aware):
bob.greet('alice', { name: 'bob', a: 1 })
bob.expect.greet.called.withArg({ name: 'bob' })
bob.expect.greet.called.withArg(match.string)All provided args must appear in a single invocation:
bob.greet('alice', 'bob')
bob.expect.greet.called.withArgs('alice', 'bob')bob.greet('The quick brown fox')
bob.expect.greet.called.withMatch(/quick.*fox/)Strict deep equality of every argument (matcher-aware):
bob.greet('alice', ['carol'], 123)
bob.expect.greet.called.matchExactly('alice', ['carol'], 123)
bob.expect.greet.called.matchExactly(match.string, match.array, match.number)Assert at least one recorded call returned expected (value or matcher):
mock.setup.sum.toReturn(42)
mock.sum(1, 2)
mock.expect.sum.called.withReturn(42)
mock.expect.sum.called.withReturn(match.gte(40))Identity check on this for at least one call:
const target = { tag: 'x' }
fn.call(target, 1)
fn.expect.called.calledOn(target)Assert at least one call threw. expected can be:
- Omitted — any throw passes
- A string — matches
Error.message - An
Errorclass — sugar formatch.instanceOf(ErrorClass) - A matcher (e.g.
match.objectContaining({ code: 1 })for non-Error throws)
mock.setup.fail.toThrow('bang')
try { mock.fail() } catch {}
mock.expect.fail.called.threw('bang')
mock.expect.fail.called.threw(Error)
mock.expect.fail.called.threw(match.instanceOf(Error))Mirrors called.* but asserts every recorded call matches (not just one). Throws if the method was never called (no vacuous-true):
bob.greet('a')
bob.greet('b')
bob.expect.greet.everyCall.withArg(match.string)
bob.expect.greet.everyCall.matchExactly(match.string)
bob.expect.greet.everyCall.withReturn(match.string)Access a specific invocation by zero-based index:
bob.greet('first')
bob.greet('second', 'extra')
bob.expect.greet.invocation(0).withArg('first')
bob.expect.greet.invocation(1).withArgs('second', 'extra')Every positive assertion has a negated form. Negation lives at the expect.method.not level:
bob.greet('alice')
bob.expect.greet.not.called.never()
bob.expect.greet.not.called.withArg('bob')
bob.expect.greet.not.called.withReturn('goodbye')
bob.expect.greet.not.called.threw()Chaining works on negated assertions too:
bob.expect.greet.not.called.once().withArg('nobody')Clear recorded calls (per method or for all methods).
Every method also exposes a read-only spy surface alongside setup / expect:
mock.spy.greet.callCount // number
mock.spy.greet.calls // readonly CallRecord[]
mock.spy.greet.firstCall // CallRecord | undefined
mock.spy.greet.lastCall // CallRecord | undefinedEach CallRecord contains args, returned, threw, thisArg, timestamp, and sequence.
expect asserts. spy reads. They overlap for simple "was this called with X?" questions — both mock.expect.greet.called.withArg('x') and mock.spy.greet.calledWith('x') inspect the same call history — but their contracts differ and so do their use cases.
| Task | Reach for |
|---|---|
| Assert a call happened in a test | expect — throws on mismatch, test fails |
| Branch on whether a call happened | spy — returns a boolean you can if on |
| Feed a return value into the next setup | spy.lastCall.returned — expect can only assert, not hand back the value |
Inspect this or a non-Error thrown value |
spy.lastCall.thisArg / .threw — the data, not just a pass/fail |
Await a captured Promise (e.g. from toResolveWith) |
await mock.spy.fetch.lastCall.returned |
| Snapshot the call log | expect(mock.spy.greet.serialize()).toMatchSnapshot() |
| Print the call log while debugging a flaky test | console.log(mock.spy.greet.printHistory()) |
| Write a custom assertion helper | spy.calls.some(...) is clean; wrapping throwing expects is not |
| Build a framework integration | deride/vitest / deride/jest are built on spy — they need structured data to hand to the host matcher |
Rule of thumb: if you're writing try { mock.expect.X.called... } catch { ... } you want spy instead. If you're writing assert(mock.spy.X.calledWith(...)) you want expect instead.
Same matching semantics as expect.withArgs, but returns a boolean instead of throwing:
if (mock.spy.greet.calledWith('alice')) { /* ... */ }Human-readable dump for debugging:
console.log(mock.spy.greet.printHistory())
// greet: 2 call(s)
// #0 greet('alice') -> 'hello alice'
// #1 greet('bob') -> 'hello bob'Stable snapshot-friendly output — keys sorted, timestamps omitted, circular refs as [Circular], functions as [Function: name]:
expect(mock.spy.greet.serialize()).toMatchSnapshot()Assert that the first call of each spy happened in the listed order:
import { inOrder } from 'deride'
inOrder(mockDb.spy.connect, mockDb.spy.query, mockLogger.spy.info)To order specific invocations rather than first-of-each, use inOrder.at:
inOrder(inOrder.at(db.spy.query, 0), inOrder.at(db.spy.query, 1))Strict variant rejects interleaved extra calls on any listed spy:
inOrder.strict(db.spy.connect, db.spy.query)Capture a mock's full state (behaviours + call history) and restore later:
const snap = bob.snapshot()
bob.setup.greet.toReturn('temporarily different')
bob.greet('x')
bob.restore(snap)
// Behaviours and call history are back to what they were at snapshot()Nested snapshots jump arbitrarily far back in time.
import 'deride/vitest'
const mock = stub<Db>(['query'])
mock.query('select')
expect(mock.spy.query).toHaveBeenCalled()
expect(mock.spy.query).toHaveBeenCalledTimes(1)
expect(mock.spy.query).toHaveBeenCalledOnce()
expect(mock.spy.query).toHaveBeenCalledWith('select')
expect(mock.spy.query).toHaveBeenLastCalledWith('select')
expect(mock.spy.query).toHaveBeenNthCalledWith(1, 'select')
// Works on MockedFunction proxies directly:
const fn = func<(x: number) => number>()
fn(5)
expect(fn).toHaveBeenCalledOnce()Same matchers, registered via jest's expect.extend:
import 'deride/jest'Lightweight, dependency-free clock. Pairs well with toResolveAfter / toHang when you don't want to pull in vi.useFakeTimers() or sinon's fake timers.
import { useFakeTimers } from 'deride/clock'
const clock = useFakeTimers()
setTimeout(() => console.log('later'), 100)
clock.tick(100) // 'later' fires now
clock.runAll() // drain any pending timers
clock.flushMicrotasks()
clock.restore()For richer behaviour (ordering-sensitive microtasks, performance.now, setImmediate), use vi.useFakeTimers() or @sinonjs/fake-timers instead.
useFakeTimers() patches Date.now, setTimeout, setInterval, and queueMicrotask on globalThis. If a test installs the clock and throws before calling restore() — or if runAll() itself throws because an active setInterval would loop forever — the patches stay in place and the next test inherits a frozen Date.now() and fake timers. This causes confusing cascading failures.
Always pair useFakeTimers() with an afterEach safety net:
import { afterEach } from 'vitest'
import { isFakeTimersActive, restoreActiveClock, useFakeTimers } from 'deride/clock'
afterEach(() => {
if (isFakeTimersActive()) restoreActiveClock()
})
it('does work with fake time', () => {
const clock = useFakeTimers()
// ... test body — even if it throws, afterEach will restore
})Or wrap the call site in a try/finally:
const clock = useFakeTimers()
try {
clock.runAll()
} finally {
clock.restore()
}The same pattern applies if you read clock.errors (errors caught from inside scheduled callbacks) — the array clears on restore(), so be sure to read it first if you need to assert on it.
class UserService {
constructor(private db: Database) {}
async getAll() { return this.db.query('SELECT * FROM users') }
}
const mockDb = deride.stub<Database>(['query'])
mockDb.setup.query.toResolveWith([{ id: 1, name: 'alice' }])
const service = new UserService(mockDb)
await service.getAll()
mockDb.expect.query.called.once()Works with whatever your test runner provides:
// Vitest
import { vi } from 'vitest'
const mockDb = deride.stub<Database>(['query'])
vi.mock('./database', () => ({ db: mockDb }))// Jest
const mockDb = deride.stub<Database>(['query'])
jest.mock('./database', () => ({ db: mockDb }))// Node test runner
import { mock } from 'node:test'
mock.module('./database', () => ({
db: deride.stub<Database>(['query']),
}))Full type support with generics:
import deride, { Wrapped } from 'deride'
interface MyService {
fetch(url: string): Promise<string>
process(data: string): void
}
const service: Wrapped<MyService> = deride.stub<MyService>(['fetch', 'process'])
service.setup.fetch.toResolveWith('response data')
service.process('hello')
service.expect.process.called.withArg('hello')Setup methods are constrained to the method's return type:
service.setup.fetch.toResolveWith('valid string') // OK
service.setup.fetch.toResolveWith(123) // Type error!To intentionally return an invalid type (e.g. testing error paths), cast with as any:
service.setup.fetch.toResolveWith(null as any)pnpm lint # eslint (src + tests)
pnpm typecheck # tsc --noEmit
pnpm test # vitest (watch mode)
pnpm build # tsup (cjs + esm + types)Copyright (c) 2014 Andrew Rea Copyright (c) 2014 James Allen
Licensed under the MIT license.
