ExisJS

exisjs/testing

The exisjs/testing module provides an integrated testing framework built on Node's native runner (node:test) and assertion engine (node:assert). It includes the createTestApp synthetic HTTP client, fluent expect assertions, Jest-compatible ex spies/mocks, and unit-level mock factories (createMockRequest, createMockResponse).


Module Exports

import {  // Test Runner & Lifecycle  test,  describe,  it,  before,  beforeAll,  after,  afterAll,  beforeEach,  afterEach,  registerCleanup,
  // Assertions & Matchers  expect,
  // Mocks, Spies & Timers  ex,  mock,
  // HTTP Integration Testing Client  createTestApp,  TestRequest,
  // Unit Mocking Helpers  createMockRequest,  createMockResponse,  createMockLogger,  createMockNext,  getResponseBody,  getResponseHeader,} from 'exisjs/testing'
import type {  TestApp,  TestRequest,  TestResponse,  TestOptions,  TestContext,  TestFunction,  HookFunction,  MockRequestOptions,  MockResponse,} from 'exisjs/testing'

1. Native Test Runner API

ExisJS enhances the native Node test runner with Jest-like .each() parameterization and typed test hooks.

// Test suites and casesdescribe(name: string, fn?: TestFunction): voidtest(name: string, fn?: TestFunction): voidtest(name: string, options: TestOptions, fn?: TestFunction): voidit(name: string, fn?: TestFunction): void
// Modifierstest.only(name: string, fn?: TestFunction): voidtest.skip(name: string, fn?: TestFunction): voidtest.todo(name: string, fn?: TestFunction): void
// Lifecycle hooksbefore(fn: HookFunction): voidbeforeEach(fn: HookFunction): voidafter(fn: HookFunction): voidafterEach(fn: HookFunction): void

Parametrized Tests (.each)

test.each([  [1, 2, 3],  [10, 20, 30],  [-5, 5, 0],])('calculates %d + %d = %d', (a, b, expected) => {  expect(a + b).toBe(expected)})

2. HTTP Integration Testing (createTestApp)

Instantiates an in-memory HTTP test client that dispatches synthetic requests into an ExisJS App instance without binding a network port.

function createTestApp(app: App | ExisAppDefinition | ExisConfig): TestApp

TestApp Interface

MethodSignatureDescription
get(path: string, options?: RequestOptions): TestRequestInitiates a synthetic GET request.
post(path: string, options?: RequestOptions | unknown): TestRequestInitiates a synthetic POST request.
put(path: string, options?: RequestOptions | unknown): TestRequestInitiates a synthetic PUT request.
patch(path: string, options?: RequestOptions | unknown): TestRequestInitiates a synthetic PATCH request.
delete(path: string, options?: RequestOptions): TestRequestInitiates a synthetic DELETE request.
options(path: string, options?: RequestOptions): TestRequestInitiates an OPTIONS preflight request.
head(path: string, options?: RequestOptions): TestRequestInitiates a HEAD request.
request(method: string, path: string, options?: RequestOptions): TestRequestDispatches an arbitrary HTTP method request.

TestRequest Builder

Fluent request builder allowing header configuration, payload attachment, and response assertions:

class TestRequest implements PromiseLike<TestResponse> {  send(body: unknown): this  set(header: string, value: string): this  expect(status: number): this  expect(header: string, value: string | RegExp): this  expect(body: unknown): this  execute(): Promise<TestResponse>}

TestResponse Interface

PropertyTypeDescription
statusnumberHTTP status code returned by the route handler.
headershttp.IncomingHttpHeadersDictionary of HTTP response headers.
bodyanyParsed JSON response payload (or undefined if not JSON).
textstringRaw UTF-8 string content of the response payload.

Integration Test Example

tests/users.test.ts
import { describe, it, expect, createTestApp } from 'exisjs/testing'import server from '../src/http/server'
describe('Users API', () => {  const client = createTestApp(server)
  it('creates and returns a user payload', async () => {    const res = await client      .post('/users', { name: 'Alice', email: '[email protected]' })      .set('Authorization', 'Bearer token_123')      .expect(201)      .expect('content-type', /application\/json/)
    expect(res.body).toMatchObject({      success: true,      data: {        name: 'Alice',        email: '[email protected]',      },    })  })})

3. Assertion Engine (expect)

The expect(actual) function returns a rich matcher engine with negative chaining via .not:

Equality & Truthiness

MatcherDescription
toBe(value)Strict referential equality (===).
toEqual(value)Deep equality check across objects and arrays.
toStrictEqual(value)Deep strict equality checking prototypes and undefined keys.
toMatchObject(subset)Verifies that the target object contains the specified subset of properties.
toBeTruthy()Asserts truthy value.
toBeFalsy()Asserts falsy value.
toBeNull()Asserts null.
toBeUndefined()Asserts undefined.
toBeDefined()Asserts not undefined.

Numeric Matchers

MatcherDescription
toBeGreaterThan(n)Asserts actual > n.
toBeGreaterThanOrEqual(n)Asserts actual >= n.
toBeLessThan(n)Asserts actual < n.
toBeLessThanOrEqual(n)Asserts actual <= n.
toBeCloseTo(n, precision?)Asserts floating point approximation.

Strings, Arrays & Errors

MatcherDescription
toContain(item)Checks if a string contains a substring or array contains an element.
toMatch(regex)Asserts string matches regular expression.
toHaveLength(length)Asserts .length property matches.
toThrow(error?)Asserts that a function throws an error matching the predicate.
toBeInstanceOf(constructor)Asserts actual instanceof constructor.

Mock Assertions

MatcherDescription
toHaveBeenCalled()Asserts the mock was called at least once.
toHaveBeenCalledTimes(count)Asserts the mock was invoked an exact number of times.
toHaveBeenCalledWith(...args)Asserts the mock was called with matching arguments.

4. Spies, Mocks & Timers (ex)

Jest-compatible API wrapping Node's native mocking subsystem:

// Function & Method Spiesconst fn = ex.fn((x: number) => x * 2)const spy = ex.spyOn(userService, 'findById')
// Mock Implementationsfn.mockReturnValue(42)fn.mockResolvedValue({ id: '1' })fn.mockRejectedValue(new Error('Failed'))fn.mockImplementation((name) => `Hello ${name}`)fn.mockClear()fn.mockRestore()
// Timer Manipulationex.setSystemTime(new Date('2026-01-01T00:00:00Z'))ex.advanceTimersByTime(5000)
// Module Mockingex.mock('./mail-client', () => ({  sendMail: ex.fn().mockResolvedValue(true),}))

5. Unit-Level Mocking Helpers

For unit testing individual middleware, guards, or filters without booting an entire server:

import {  createMockRequest,  createMockResponse,  createMockNext,  getResponseBody,  getResponseHeader,} from 'exisjs/testing'
const req = createMockRequest({  method: 'POST',  url: '/api/v1/checkout',  headers: { 'x-tenant-id': 'tenant_123' },  body: { amount: 5000 },})
const res = createMockResponse()const next = createMockNext()
await authGuard(req, res, next)
expect(next.called).toBe(true)