RavenDB test driver for Node.js.
Starts one embedded RavenDB server per test process and hands out an isolated, auto-deleted database per getDocumentStore() call.
npm install --save-dev ravendb-testdriveror with pnpm / yarn:
pnpm add -D ravendb-testdriveryarn add -D ravendb-testdriverRequires Node.js >= 22.
import { afterAll, describe, expect, it } from "vitest";
import { RavenTestDriver, TestServerOptions } from "ravendb-testdriver";
// Safe to call from every test file: first registration wins, later calls
// are no-ops, and the factory runs once when the server actually starts.
RavenTestDriver.configureServerOnce(() => new TestServerOptions({
licensing: {
eulaAccepted: true,
license: process.env.RAVEN_LICENSE,
},
}));
const driver = new RavenTestDriver({
setupDatabase: async (store) => {
const session = store.openSession();
await session.store({ name: "Grisha" }, "people/1");
await session.saveChanges();
},
});
afterAll(async () => {
await driver.dispose();
});
describe("people", () => {
it("loads seeded data", async () => {
const store = await driver.getDocumentStore();
const session = store.openSession();
const person = await session.load("people/1");
expect(person?.name).toBe("Grisha");
});
});Subclassing works too: override preInitialize, preConfigureDatabase, setupDatabase, or the databaseDumpFilePath / databaseDumpFileStream getters.
examples/ holds three standalone, runnable projects, one per test runner. Each covers the same seven scenarios with the same test bodies, so the difference between folders is the runner setup, not the driver usage.
| Runner | Folder | Setup needed | Servers per run |
|---|---|---|---|
| vitest | examples/vitest | pool: "forks" + fileParallelism: false; raised testTimeout / hookTimeout |
1 |
| mocha | examples/mocha | No --parallel; raised timeout; one after hook calling stopServer() |
1 |
| jest | examples/jest | --experimental-vm-modules, --runInBand, raised testTimeout, stopServer() per file |
1 per file |
The scenarios: basic CRUD; a fresh database per store (and await using); the setupDatabase / preInitialize / preConfigureDatabase hooks plus subclassing; waitForIndexing and querying a static index; seeding from a .ravendbdump by path and by stream; the server lifecycle (configureServerOnce vs configureServer, lazy start, server reuse); and an HTTPS server with certificate authentication via ServerOptions.secured().
Build the package first, since each example depends on it via file:../..:
pnpm buildThen run any folder:
cd examples/vitest && npm install && npm testStart with the vitest example if you have no runner preference, since it needs the least setup. See examples/README.md for the runner comparison, and each folder's README for its own caveats.
Two notes that apply to every folder:
- Raise the runner's timeout. The first
getDocumentStore()call starts the embedded server, and on the very first run downloads the RavenDB binaries, which takes far longer than any runner's default (vitest and jest: 5s, mocha: 2s). Each example sets 300s:testTimeout+hookTimeoutfor vitest,testTimeoutfor jest,timeoutfor mocha. - The jest and mocha examples pin TypeScript
~5.9.ts-jestandts-nodeboth cap their TypeScript peer below 7, so TypeScript 7 fails their install with anERESOLVEpeer conflict. The driver itself builds on TypeScript 7; the pin is a constraint of those two loaders, not of the driver.
The driver keeps one embedded RavenDB server per Node process (a module-level singleton). Every getDocumentStore() call creates a fresh, isolated database against that same server and deletes it when the returned store is disposed.
More precisely, there is one server per module registry. Test runners that spawn one worker process per file (vitest's and jest's default) therefore start one server per file instead of one per run. This repo's own vitest.config.ts avoids that by forcing a single worker process:
// vitest.config.ts
export default defineConfig({
test: {
pool: "forks",
fileParallelism: false,
// ...
},
});(poolOptions.forks.singleFork doesn't apply here: Vitest 4 removed it. pool: "forks" combined with fileParallelism: false is the equivalent: a single fork runs every test file sequentially, so the module-level server survives across files.)
Mocha gets the same result by default, as long as you don't enable --parallel. Jest is the exception: it isolates the module registry per test file, so each file starts its own server no matter the configuration, --runInBand included. See the "Jest" section below.
If you need multiple concurrent servers instead (e.g. to parallelize across workers), see the "Internal/advanced" note below.
With Jest, the single-worker equivalent is --runInBand, and two more caveats apply (a working setup is examples/jest):
-
Work around the client's dynamic
import()calls. Theravendbclient loads some of its dependencies through dynamicawait import()(e.g.undicifor its HTTP agent), which Jest's VM sandbox rejects, so every request then fails with a 503-wrapped dispatcher assertion. Two ways around it, pick one:Option 1 (recommended): run Jest with
--experimental-vm-modules. Enables dynamicimport()inside Jest's sandbox, so all client features work, with no other changes needed:{ "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand" } }(Or add the flag to
NODE_OPTIONS, which is also the fix for IDE test runners that bypass the npm script.)Option 2: a
setupFilesscript that swaps the client's agent factory for arequire-based one. No Node flag needed, but note that no example implements this path, so unlike Option 1 it is not covered by a runnable project here. Addundicias a devDependency (package managers with strictnode_moduleslayouts, like pnpm, won't resolve transitive packages):// test/jest.setup.ts import { Agent } from "undici"; import { RequestExecutor } from "ravendb"; (RequestExecutor as any).createAgent = async (options: Agent.Options) => new Agent(options);
// jest.config.js module.exports = { // ... setupFiles: ["<rootDir>/test/jest.setup.ts"], };
This covers the plain CRUD/query/indexing path, but not features that hide other dynamic imports (bulk insert via
node:zlib, the Changes API viaws, smuggler import/export vianode:fs), which is why Option 1 is the recommendation.A future
ravendbclient release is expected to drop the dynamic imports on the CommonJS path, making both workarounds unnecessary. -
Stop the server in every test file, or Jest hangs after a green run. The embedded server deliberately survives
driver.dispose()so it can be reused, and is normally cleaned up at process exit. Jest runs tests in-process, so the Raven.Server child-process handle keeps the event loop alive and Jest reportsJest did not exit one second after the test run has completed.and then hangs (in CI, until the job times out). CallRavenTestDriver.stopServer()in each file'safterAll:afterAll(async () => { await driver.dispose(); await RavenTestDriver.stopServer(); });
Per file, and not in a
globalTeardown: Jest gives each test file its own module registry, so the driver's module-level server singleton is per-file too. AglobalTeardownruns in yet another registry, where it sees no server and stops nothing: the run still hangs and each file's server leaks until the process is killed. Only the registry that started a server can stop it.The same isolation means each Jest test file starts its own embedded server, even under
--runInBand. That is inherent to Jest; for a single server per run, use vitest (pool: "forks"+fileParallelism: false) or Mocha without--parallel.
Vitest with pool: "forks" needs neither: the fork loads undici natively and exits cleanly, which is why the vitest example above stops at driver.dispose().
Mocha runs every test file in one process by default, so the shared server already behaves as one-per-run, so just don't enable --parallel. A working setup is examples/mocha. Two caveats:
-
Raise the timeout. The first
getDocumentStore()call starts (and on first ever run, downloads) the embedded server, which blows past Mocha's default 2s timeout:// .mocharc.json { "require": "ts-node/register", "spec": "test/**/*.test.ts", "timeout": 300000 }
-
Stop the server explicitly or Mocha hangs after a green run. Same in-process issue as Jest: the Raven.Server child-process handle keeps the event loop alive. Call
RavenTestDriver.stopServer()in a root-levelafterhook:import * as assert from "node:assert/strict"; import { RavenTestDriver, TestServerOptions } from "ravendb-testdriver"; RavenTestDriver.configureServerOnce(() => new TestServerOptions({ licensing: { eulaAccepted: true, license: process.env.RAVEN_LICENSE, }, })); const driver = new RavenTestDriver({ setupDatabase: async (store) => { const session = store.openSession(); await session.store({ name: "Grisha" }, "people/1"); await session.saveChanges(); }, }); after(async () => { await driver.dispose(); await RavenTestDriver.stopServer(); }); describe("people", () => { it("loads seeded data", async () => { const store = await driver.getDocumentStore(); const session = store.openSession(); const person = await session.load<{ name: string }>("people/1"); assert.equal(person?.name, "Grisha"); }); });
Unlike Jest, Mocha has no VM sandbox: tests run on plain Node, so the ravendb client's dynamic import("undici") works out of the box, with no setup-file workaround or --experimental-vm-modules flag (see the Jest section above).
licensing.throwOnInvalidOrMissingLicense defaults to true unless you explicitly set it in the constructor init.
licensing.eulaAccepted = trueis required for the embedded server to auto-download the RavenDB server binaries.- Provide a license via
licensing.license(inline string) orlicensing.licensePath(path to a license file). - This repo's own integration tests read the license from the
RAVEN_LICENSE/RAVEN_LICENSE_PATHenvironment variables (seetest/testOptions.ts); when neither is set, they fall back tothrowOnInvalidOrMissingLicense = falseso the suite can still run unlicensed locally.
RavenTestDriver.configureServer(options): configures the shared per-process server. Must run before the firstgetDocumentStore(); throws if called afterward.RavenTestDriver.configureServerOnce(factory): idempotent variant for setup code that runs once per test file: the first registered factory wins and every later call (even after the server started) is a no-op, so callers don't need their own "already configured" guard. The factory runs lazily, at most once, when the server actually starts, so side effects likemkdtempSyncor reading a license file don't repeat per file. An explicitconfigureServercall before start overrides a pending factory.RavenTestDriver.stopServer(): stops the shared per-process server (no-op if none was started) and resets its configuration soconfigureServercan be called again. Unnecessary under vitest withpool: "forks"(the fork exits and takes the server with it), but required with in-process runners: once in a root-levelafterfor Mocha, and in every test file'safterAllfor Jest (see the "Jest" and "Mocha" sections above).driver.getDocumentStore(options?, database = "test"): creates a new isolated database and returns a store for it; the database is deleted when the store is disposed.options.waitForIndexingTimeoutwaits for indexing to settle before returning.driver.waitForIndexing(store, database?, timeout?): waits until all indexes are non-stale (default timeout 60s), throwing (with index error details) on timeout.driver.waitForUserToContinueTheTest(store): opens RavenDB Studio for the store's database and resumes once you create aDebug/Donedocument from Studio. Warning: this blocks forever until that document appears; it never resumes on its own. Treat it as an interactive/debug-only helper and never leave a call to it in committed test code, or CI will hang indefinitely.driver.dispose(): disposes all stores handed out by this driver (and any scoped server). Also available viaawait using driver = new RavenTestDriver()(TypeScript 5.2+ /ESNext.Disposablelib, sinceRavenTestDriverimplementsAsyncDisposable). Disposal failures are collected and thrown together as anAggregateError.driver.isDisposed: whetherdispose()has finished.getDocumentStore()throws once the driver is disposed.driver.onDriverDisposed: optional callback invoked once, afterdispose()completes (the C#DriverDisposedevent).TestServerOptions.useFiddler(): static factory returning options bound tohttp://<hostname>:0withSecurity.UnsecuredAccessAllowed=PrivateNetwork, so the traffic is visible to a proxy such as Fiddler.
For a server secured with ServerOptions.secured() / securedWithExec(), the driver passes the client certificate to every store it hands out, so test code itself never mentions certificates. See scenario 07 in the examples.
pnpm build
pnpm testpnpm test:unit and pnpm test:integration run the two suites separately. Integration tests spin up a real embedded server, so they need a RavenDB license (or RAVEN_LICENSE/RAVEN_LICENSE_PATH unset, which runs unlicensed).
The examples are separate projects that consume the built package via file:../.., so they are not covered by pnpm test. Run pnpm build first, then npm install && npm test inside the folder you want to check.