Nano Spy

June 19, 2026 · View on GitHub

A tiny Node.js library to spy and mock methods in tests with great TypeScript support.

It will take only 6 KB in your node_modules and have 0 dependencies.

import { spyOn, restoreAll } from 'nanospy'

test.after.each(() => {
  restoreAll()
})

test('calls increase', () => {
  const spy = spyOn(counter, 'increase')
  counter.increase(5)
  assert.equal(spy.callCount, 1)
  assert.equal(spy.calls, [[5]])
})
Sponsored by Evil Martians

Usage

using Syntax

In Node.js 24+ (or any runtime with explicit resource management support), you can use the using declaration to automatically restore the spy when it goes out of scope:

import { spyOn } from 'nanospy'

test('calls increase', () => {
  using spy = spyOn(counter, 'increase')
  counter.increase(5)
  assert.equal(spy.callCount, 1)
}) // `counter.increase` is restored automatically here

This removes the need for restoreAll() in an afterEach hook for spies declared with using.

Method Spy

Spy tracks method calls, but do not change method’s behavior.

import { spyOn, restoreAll } from 'nanospy'

test.after.each(() => {
  restoreAll()
})

test('calls increase', () => {
  const spy = spyOn(counter, 'increase')
  counter.increase(5)
  assert.is(spy.called, true)
  assert.equal(spy.callCount, 1)
  assert.equal(spy.calls, [[5]])
  assert.equal(spy.results, [true])
})

Mock

Mock change the method’s behavior.

const spy = spyOn(global, 'fetch', async () => {
  return {
    json: () => ({ posts })
  }
})

Or change next function call:

spy.nextResult({ ok: false })
spy.nextError(error)

Functions

spy can be used to track callback calls.

import { spy } from 'nanospy'

const fn = spy()
fn('a', 10)
fn.callCount //=> 1
fn.calls //=> [['a', 10]]

You can pass spy’s callback:

let fn = spy((name: string) => {
  console.log(`Hello, ${name}!`)
})
fn('Ivan') //=> Hello, Ivan!

Or change next function call:

fn.nextResult({ ok: false })
fn.nextError(error)

Promises

You can use helpers to test promises:

import { spy } from 'nanospy'

const fn = spy()

let resolve = fn.nextResolve()
fn().then(arg => {
  console.log('Resolved ' + arg)
})

await resolve(1) // => Resolved 1

For testing errors, you can use fn.nextReject().

Remocking

You can reassign mocked function with onCall method:

const obj = {
  mark: str => str + '!'
}
const spy = spyOn(obj, 'mark')

obj.mark('a')
assert.equal(spy.results, ['a!'])

spy.onCall(str => str + '?')
obj.mark('a')
assert.equal(spy.results, ['a!', 'a?'])