Testing
ExisJS was built with testing as a first-class citizen. Instead of forcing you to configure bulky external test runners like Jest or Vitest, ExisJS natively wraps the highly optimized, built-in Node.js Test Runner (node:test).
This means you get highly optimized, integrated testing out-of-the-box.
The Testing Module
ExisJS exports everything you need for testing directly from exisjs/testing. This includes the test runner functions (test, describe, it, before), assertions (assert), and the powerful ExisJS createTestContext!
Creating a Test Context
When testing an API, you want to boot the app, run HTTP requests against it, and then gracefully shut it down (closing database connections so the test runner doesn't hang). createTestContext does exactly this for you automatically! It seamlessly handles both functional exis({ ... }) applications and Object-Oriented @Server decorated classes.
import { describe, it, expect, createTestContext } from 'exisjs/testing'import app from '../src/http/server' // Import your Exis instance or @Server class
describe('Users API', () => { // Automatically boots the app, runs `onStart`, and gracefully shuts down databases after tests! const api = createTestContext(app)
it('should create a new user', async () => { // `api` exposes a fast, fluid HTTP client const res = await api .post('/users') .send({ name: 'Alice', email: '[email protected]' }) .execute()
// Integrated Jest-like expectations expect(res.status).toBe(201) expect(res.body.success).toBe(true) expect(res.body.data.name).toBe('Alice') })
it('should return 400 on invalid input', async () => { const res = await api .post('/users') .send({ name: 'Bob' }) // Missing email .execute()
expect(res.status).toBe(400) expect(res.body.success).toBe(false) })})Running Tests
Because ExisJS wraps the native Node.js test runner in its powerful CLI, you can run all your tests simply using the standard Exis command!
npx exisjs testOr configure it in your package.json:
{ "scripts": { "test": "exisjs test" }}The exisjs test command automatically bootstraps your environment, loads your environment variables (.env), registers TypeScript (tsx), and recursively finds and executes all test files matching standard testing conventions (*.test.ts, *.spec.ts, etc.) regardless of where they are placed.
Test Configuration
ExisJS allows you to configure your testing environment directly. By default, it will use your project's TypeScript configuration, but you can override this by creating a specific tsconfig.test.json file in your project root. The test runner will automatically detect and prioritize it when running tests!
Mocking (Dependency Injection)
If you are using ExisJS's built-in Dependency Injection (DI) or Gateways, mocking is incredibly easy. You don't need magic jest.mock() intercepts. You can simply override the providers in your test!
import { describe, it, expect, createTestContext } from 'exisjs/testing'import app from '../src/http/server'import { BillingGateway } from '../src/http/billing/gateway'
describe('Billing API', () => { // Override the StripeService strictly for testing! BillingGateway.overrideProvider('StripeService', { charge: async (amount: number) => ({ success: true, transactionId: 'test_123' }) })
const api = createTestContext(app)
it('should process payment', async () => { const res = await api .post('/billing/charge') .send({ amount: 500 }) .execute()
expect(res.status).toBe(200) expect(res.body.transactionId).toBe('test_123') })})Test Isolation
One of the most common pitfalls when testing backend frameworks is test bleed—where the state from one test unexpectedly affects the next. ExisJS provides the tools to prevent this, but you must be intentional about state isolation.
1. Dependency Injection Cleanup
If you rely heavily on singletons in the ExisJS Dependency Injection container, you may want to reset the container's state between tests.
import { beforeEach } from 'exisjs/testing'import { getActiveApp } from 'exisjs/app'
beforeEach(() => { const app = getActiveApp() if (app) { app.container.clearCache() // Purges all instantiated singletons }})2. Zero-Config Singletons (Redis, Prisma, etc.)
ExisJS provides zero-config integrations (redis.ts, prisma.ts, etc.) which act as process-wide singletons.
Because they use proxy-based caching at the module level, running multiple test suites concurrently in the same Node.js process means they will all share the exact same database connections and state.
If your tests write to the database or queue, ensure you either:
- Run your test runner in a mode that isolates processes per file.
- Manually clear database tables or flush Redis in a
beforeEachblock. - Use database transactions that rollback after each test.