fake-indexeddb
fake-indexeddb is a dependency-free, in-memory JavaScript implementation of the browser IndexedDB API, built mainly for unit tests that run in Node. It supplies indexedDB plus the standard IDB classes, supports both automatic global installation and explicit imports, and works with wrappers such as Dexie and idb. Its data lasts only inside the current JavaScript process and factory instance. This is a test double for storage behavior, not a persistent database and not a complete browser environment.
Use fake-indexeddb for fast unit coverage around code that speaks IndexedDB, especially when dependencies can be injected. Keep real-browser tests for browser scheduling, storage policy, and the conformance gaps its own README measures.
Use it if
- You unit-test code that calls IndexedDB but want tests to run quickly in Node without launching a browser
- You need a fresh, isolated IDBFactory for each test or suite so database names and records cannot leak between cases
- You use Dexie or idb and want to inject an IndexedDB-compatible backend in tests
- You need to exercise upgrades, object stores, indexes, cursors, transactions, and abnormal close handling in memory
- You need persistence across processes or restarts: the README states that data is not persisted to disk and every factory is only in-memory state
- Browser fidelity is the acceptance criterion: version 6.2.5 passes 1,369 of 1,653 listed IndexedDB Web Platform Tests, while the same table reports Chrome passing 1,651
- Your tests depend on workers, cross-origin isolation, quota behavior, storage eviction, or browser scheduling: the README says some browser-only Web Platform Tests are omitted and polyfills simulate APIs such as File and location
- You are on Node older than 18: the current package metadata requires Node 18 or newer, and modern globals are no longer polyfilled for old environments
- You want one broad DOM test environment: this package installs IndexedDB interfaces only, so it does not replace jsdom, happy-dom, or a real browser for document, navigation, and layout behavior
Setup reality
Install it as a development dependency. The easiest setup is importing fake-indexeddb/auto before application code; that mutates the global scope with indexedDB and all IDB constructors, so import order matters for wrappers that capture globals at module load time. Dexie must see the auto import first, or receive indexedDB and IDBKeyRange explicitly in its constructor. For Jest, put fake-indexeddb/auto in setupFiles rather than setupFilesAfterEnv when modules need the globals during import. Version 6 supports both ESM imports and CommonJS require through explicit export mappings and includes TypeScript declarations based on TypeScript's built-in IndexedDB types. The package requires Node 18 or newer. Since version 5 it does not ship a structuredClone polyfill. Node itself has structuredClone, but jsdom can expose a separate global that lacks it; the documented fixes are to import core-js/stable/structured-clone before fake-indexeddb or copy Node's implementation into a custom Jest environment. State is shared for as long as one IDBFactory lives. Closing database connections does not erase databases, so test isolation means calling deleteDatabase and waiting for success or replacing the factory with new IDBFactory(). Outstanding connections can block upgrades and deletion just as they do in browsers, which means cleanup must close handles. Transactions and requests remain event-based, so helper promises must listen for error, abort, and complete rather than assuming a successful request means the entire transaction committed. forceCloseDatabase exists for close-event coverage, but it is a package-specific testing extension and must never enter browser production code. Finally, conformance is good but incomplete: keep at least a small real-browser test layer for upgrade races, quota, workers, and any bug that depends on browser task scheduling.
Patterns
Install IndexedDB globals for a testinstall-global-api
import 'fake-indexeddb/auto'
const request = indexedDB.open('app-test', 1)Import this before modules that read indexedDB at module initialization. It mutates globalThis for the rest of the test environment.
Use an isolated factory without globalsuse-explicit-factory
import {IDBFactory, IDBKeyRange} from 'fake-indexeddb'
const testIndexedDB = new IDBFactory()
const request = testIndexedDB.open('isolated', 1)
console.log(IDBKeyRange.only('active'))A separate IDBFactory owns separate in-memory databases. Explicit injection avoids cross-test global state.
Create stores and indexes during upgradecreate-database-schema
import {indexedDB} from 'fake-indexeddb'
const request = indexedDB.open('library', 1)
request.onupgradeneeded = () => {
const store = request.result.createObjectStore('books', {keyPath: 'isbn'})
store.createIndex('by_title', 'title', {unique: true})
}
request.onsuccess = () => console.log('opened', request.result.name)Schema changes belong in onupgradeneeded. Keep older connections closed or later version upgrades can be blocked.
Turn an IDB request into a promiseawait-idb-request
function requestToPromise(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
const db = await requestToPromise(indexedDB.open('library', 1))This helper waits for one request only. A successful write request can precede the enclosing transaction's final commit.
Write a record and await transaction commitwrite-and-commit
const tx = db.transaction('books', 'readwrite')
const store = tx.objectStore('books')
store.put({isbn: '978-1', title: 'The Test Book'})
await new Promise((resolve, reject) => {
tx.oncomplete = resolve
tx.onerror = () => reject(tx.error)
tx.onabort = () => reject(tx.error ?? new Error('transaction aborted'))
})Wait for tx.oncomplete when the test cares that all writes committed. Waiting only for put().onsuccess is too early.
Read a record through an indexquery-index
const tx = db.transaction('books', 'readonly')
const request = tx.objectStore('books').index('by_title').get('The Test Book')
const book = await requestToPromise(request)
console.log(book.isbn)Index names and uniqueness behave like IndexedDB, including constraint errors, but keep a real-browser test for edge-case conformance.
Iterate records with a bounded cursoriterate-cursor
import {IDBKeyRange} from 'fake-indexeddb'
const store = db.transaction('books').objectStore('books')
const request = store.openCursor(IDBKeyRange.lowerBound('978-1'))
request.onsuccess = () => {
const cursor = request.result
if (!cursor) return
console.log(cursor.key, cursor.value)
cursor.continue()
}Cursor iteration is event-driven. The request fires success again after each continue() and once more with a null result at the end.
Replace state between test casesreset-between-tests
import {IDBFactory} from 'fake-indexeddb'
let testIndexedDB
beforeEach(() => {
testIndexedDB = new IDBFactory()
})Replacing the factory is the simplest full reset. Application code must receive this instance rather than retaining a previously imported global.
Install globals for every Jest testconfigure-jest
export default {
setupFiles: ['fake-indexeddb/auto'],
testEnvironment: 'node'
}Use setupFiles so globals exist before test modules load. A jsdom environment may also need a structuredClone fix.
Provide structuredClone in jsdompolyfill-structured-clone
import 'core-js/stable/structured-clone'
import 'fake-indexeddb/auto'Import order matters. Since fake-indexeddb 5, the package does not include this polyfill; core-js is a separate dependency.
Use Dexie without changing globalsinject-into-dexie
import Dexie from 'dexie'
import {indexedDB, IDBKeyRange} from 'fake-indexeddb'
const db = new Dexie('MyDatabase', {indexedDB, IDBKeyRange})
db.version(1).stores({friends: '++id,name'})Explicit injection is safer for parallel tests. If you use fake-indexeddb/auto instead, import it before Dexie.
Trigger an abnormal close eventsimulate-abnormal-close
import {forceCloseDatabase} from 'fake-indexeddb'
const closed = new Promise((resolve) => db.addEventListener('close', resolve, {once: true}))
forceCloseDatabase(db)
await closedforceCloseDatabase() is a fake-indexeddb testing extension, not a standard IndexedDB API and not portable to browsers.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| indexeddbshim | npm | You need an IndexedDB shim for environments backed by another storage mechanism rather than a Node-only in-memory test double |
| @web/test-runner | npm | You want unit-style tests executed in real browsers so the native IndexedDB implementation is part of the test |
| playwright | npm | You need end-to-end coverage of persistence, browser profiles, workers, or upgrade behavior in Chromium, Firefox, and WebKit |
| memory-level | npm | You only need an in-memory key-value database for Node tests and do not need the IndexedDB API contract |