ExisJS

Testing

ExisJS provides an integrated testing suite built on top of the native Node.js Test Runner (node:test) and TypeScript execution environment (tsx). You can execute unit tests, mock dependencies, and perform full-pipeline HTTP integration testing with zero external test runners.


The Testing Module (exisjs/testing)

The exisjs/testing module re-exports test runner primitives, assertions, mocking helpers, and the HTTP testing harness:

ExportSource / RoleDescription
describe, test, itnode:testTest suite and test case definition functions with .only, .skip, .todo, and .each.
before, after, beforeEach, afterEachnode:testLifecycle execution hooks.
expectExisJS matcher engineJest-compatible fluent assertion matchers (toBe, toEqual, toMatchObject, toThrow, etc.).
createTestContext(app)ExisJS HTTP harnessBoots an Exis application instance, sets up lifecycle hooks, and returns a fluent TestApp client.
createTestApp(app)ExisJS HTTP harnessWraps an existing App or ExisAppDefinition in a TestApp client without global test hooks.
createMockRequest(opts)ExisJS mock helperCreates a synthetic ExisRequest for unit-testing controllers or middlewares.
createMockResponse()ExisJS mock helperCreates a synthetic ExisResponse capturing status codes and output buffers.
exExisJS mocking suiteMock functions (ex.fn), method spies (ex.spyOn), module mocks (ex.mock), and fake timers (ex.useFakeTimers).
registerCleanup(fn)ExisJS cleanupRegisters a teardown callback invoked after test context completion.

Integration Testing with createTestContext

createTestContext initializes your application definition, executes onStart hooks, establishes database connections (Mongoose, Prisma, or DatabaseManager), and tears down connections gracefully in afterAll.

Tests always load routes from your source files, never from a previous exisjs build. exisjs test reads .env.test on top of .env, so point it at a separate database. A test file that crashes while loading is reported as failed, and a run that finds no test files fails (set EXIS_PASS_WITH_NO_TESTS=1 to allow it).

Functional Paradigm Application Test

Given a functional application defined in src/http/server.ts:

tests/users.test.ts
import { describe, it, expect, createTestContext } from 'exisjs/testing'import app from '../src/http/server'
describe('Users API Integration', () => {  // Automatically boots the app and manages database lifecycle  const api = createTestContext(app)
  it('should create a new user record', async () => {    const res = await api      .post('/users')      .set('authorization', 'Bearer test-token')      .send({        name: 'Jane Doe',        email: '[email protected]'      })      .expect(201)
    expect(res.body).toMatchObject({      success: true,      data: {        id: expect.any(String),        name: 'Jane Doe',        email: '[email protected]'      }    })  })
  it('should return 400 when payload validation fails', async () => {    const res = await api      .post('/users')      .send({ name: 'Jane Doe' }) // missing email
    expect(res.status).toBe(400)    expect(res.body.success).toBe(false)    expect(res.body.error).toBe('Validation failed')  })})

Class-Based (OOP) Application Test

createTestContext natively supports @Server decorated entry classes:

tests/server.test.ts
import { describe, it, expect, createTestContext } from 'exisjs/testing'import ServerApp from '../src/http/server'
describe('OOP Server API Integration', () => {  const api = createTestContext(ServerApp)
  it('should retrieve status check', async () => {    const res = await api.get('/health').expect(200)
    expect(res.body).toEqual({      status: 'ok',      uptime: expect.any(Number)    })  })})

Fluid Request Builder (TestRequest)

The client returned by createTestContext and createTestApp provides request builder methods for standard HTTP verbs:

const res = await api  .get(path)       // .post(path), .put(path), .patch(path), .delete(path), .head(path), .options(path)  .set('x-api-key', 'secret-key')  .send({ key: 'value' })  .expect(200)  .expect('content-type', /json/)  .execute()

Two more builder methods:

  • .query({ page: 2, tags: ['a', 'b'] }) appends a query string.
  • .timeout(ms) fails the request after ms milliseconds (default 5000, or EXIS_TEST_TIMEOUT; 0 waits forever). A handler that never answers fails its own test with the route in the message instead of hanging the whole run.

Response Object Properties

The returned TestResponse contains:

export interface TestResponse {  status: number                       // HTTP response status code (e.g. 200, 404)  headers: http.IncomingHttpHeaders     // Normalized response headers object  body: any                            // Parsed JSON payload or raw buffer/string  text: string                         // Raw response string representation}

Testing Over Real Sockets (startTestServer)

In-memory requests cover most tests. For what only a real connection shows (CORS and security headers as a browser sees them, keep-alive, malformed HTTP), start the app on a free port:

tests/headers.test.ts
import { describe, it, expect, startTestServer } from 'exisjs/testing'import app from '../src/http/server'
describe('headers', () => {  it('sends a Content-Security-Policy', async () => {    const server = await startTestServer(app)    try {      const res = await server.request('/health')      expect(res.headers.get('content-security-policy')).toBeDefined()    } finally {      await server.close()    }  })})

exisjs test runs your source files. To test the production build itself, run exisjs build, then start it with exisjs start (which always sets NODE_ENV=production) and send requests to it.

Cron Jobs in Tests

Under NODE_ENV=test (what exisjs test sets) cron jobs and timeout tasks are registered but never started, so a test run cannot fire a real job. Run one on demand, or opt in for a test file:

await app.cron.trigger('daily-cleanup') // runs it once, now
const api = createTestContext(app, { cron: true }) // schedules jobs as in production

Unit Testing Controllers & Middlewares

For fast unit tests that do not require booting a full HTTP server pipeline, use createMockRequest and createMockResponse:

tests/unit/guard.test.ts
import { describe, it, expect } from 'exisjs/testing'import { createMockRequest, createMockResponse, createMockNext } from 'exisjs/testing'import { authGuard } from '../../src/common/guards/auth'
describe('Auth Guard Unit Test', () => {  it('should reject requests missing Authorization header', async () => {    const req = createMockRequest({      method: 'GET',      url: '/protected',      headers: {}    })    const res = createMockResponse()    const next = createMockNext()
    await authGuard(req, res, next)
    expect(res._statusCode).toBe(401)    expect(next.called).toBe(false)  })
  it('should proceed when Authorization header is present', async () => {    const req = createMockRequest({      method: 'GET',      url: '/protected',      headers: {        authorization: 'Bearer valid-jwt-token'      }    })    const res = createMockResponse()    const next = createMockNext()
    await authGuard(req, res, next)
    expect(next.called).toBe(true)  })})

Mocking & Spies (ex)

ExisJS includes a built-in mocking engine via ex:

tests/service.test.ts
import { describe, it, expect, ex } from 'exisjs/testing'import { PaymentService } from '../src/http/billing/service'
describe('PaymentService Unit Test', () => {  it('should record transactions through payment gateway', async () => {    const mockGateway = {      charge: ex.fn().mockResolvedValue({ id: 'tx_123', status: 'paid' })    }
    const service = new PaymentService(mockGateway as any)    const result = await service.processOrder('order_456', 5000)
    expect(mockGateway.charge).toHaveBeenCalledTimes(1)    expect(mockGateway.charge).toHaveBeenCalledWith(5000)    expect(result.id).toBe('tx_123')  })})

Fake Timers

import { describe, it, expect, ex } from 'exisjs/testing'
describe('Timer operations', () => {  it('advances timer controlled tasks', () => {    ex.useFakeTimers()    const spy = ex.fn()
    setTimeout(spy, 10_000)
    ex.advanceTimersByTime(10_000)    expect(spy).toHaveBeenCalledTimes(1)
    ex.useRealTimers()  })})

Running Tests via CLI

Run your test suites with the built-in exisjs test command:

# Run all test files matching tests/**/*.test.ts or tests/**/*.spec.tsnpx exisjs test
# Run a specific test filenpx exisjs test tests/users.test.ts
# Run tests in watch modenpx exisjs test --watch
# Run with test coveragenpx exisjs test --coverage

package.json Configuration

Add the test command to your scripts:

package.json
{  "scripts": {    "test": "exisjs test",    "test:watch": "exisjs test --watch"  }}

Test Configuration Options (exis.config.ts)

You can customize test runner behaviors in exis.config.ts:

exis.config.ts
import { defineConfig } from 'exisjs/config'
export default defineConfig({  test: {    include: ['tests/**/*.test.ts', 'src/**/*.spec.ts'],    concurrency: true,    coverage: false,    setupFiles: ['./tests/setup.ts']  }})