title: 'Testing · Nael Framework' description: 'Unit and integration test Nael applications with @nl-framework/testing: an overridable testing module plus in-process HTTP, GraphQL, and microservice harnesses — no ports, no Mongo, no Dapr.'

Fundamentals

Testing

@nl-framework/testing mirrors @nestjs/testing: build an application context with overridable providers, then exercise your HTTP, GraphQL, and microservice handlers entirely in-process. No port is ever bound, and the suite runs with no Mongo, no Dapr, and no network.

Installation

Add the package as a dev dependency. @nl-framework/core is a required peer; @nl-framework/http, @nl-framework/graphql, and @nl-framework/microservices are optional peers, loaded lazily — you only need the transports you actually test.

Install
bun add -d @nl-framework/testing

The testing module

Test.createTestingModule() collects your module definition, then compile() bootstraps a real ApplicationContext. Resolve providers with get(), and tear everything down — running the shutdown lifecycle — with close().

Create and tear down a module
import { Test } from '@nl-framework/testing';const moduleRef = await Test.createTestingModule({  imports: [AppModule],  providers: [/* extra providers */],}).compile();const service = await moduleRef.get(UserService);// ...assertions...await moduleRef.close(); // runs onModuleDestroy for every instantiated provider

Overrides

Substitute any provider, guard, interceptor, filter, or pipe. Each override terminal accepts .useValue(), .useClass(), or .useFactory({ factory, inject }). Because guards, interceptors, filters, and pipes all resolve through the container at runtime, overriding them is the same token swap as overriding a provider.

Fluent override chain
const moduleRef = await Test.createTestingModule({ imports: [AppModule] })  .overrideProvider(UserService).useValue(mockUserService)  .overrideProvider(CONFIG_TOKEN).useFactory({ factory: () => testConfig })  .overrideGuard(AuthGuard).useValue({ canActivate: () => true })  .overrideInterceptor(CacheInterceptor).useValue(passthroughInterceptor)  .overrideFilter(HttpExceptionFilter).useClass(TestFilter)  .compile();

Overrides are registered before any provider is instantiated, so dependents receive the mock — not the original — when they are constructed.

Downstream injectors receive the mock
@Injectable()class GreetingService {  greet(name: string) {    return `Hello ${name}`;  }}@Injectable()class GreetingConsumer {  constructor(private readonly service: GreetingService) {}  run() {    return this.service.greet('world');  }}const moduleRef = await Test.createTestingModule({ imports: [AppModule] })  .overrideProvider(GreetingService)  .useValue({ greet: (name: string) => `Mocked ${name}` })  .compile();const consumer = await moduleRef.get(GreetingConsumer);// The consumer was constructed with the mock, not the real service.expect(consumer.run()).toBe('Mocked world');

HTTP integration testing

createHttpApplication() returns a client that dispatches requests straight through the routing pipeline (middleware → guards → interceptors → pipes → handler → filters) via HttpApplication.handle. No port is bound.

Driving routes in-process
const app = await moduleRef.createHttpApplication();// Raw Responseconst res = await app.request('/users/42', {  headers: { 'x-token': 'secret' },});expect(res.status).toBe(200);// Parsed JSON helper (serializes the body, defaults content-type)const { status, body } = await app.requestJson('/users', {  method: 'POST',  json: { name: 'Grace' },});expect(status).toBe(200);expect(body).toEqual({ id: 'mock', name: 'Grace' });

GraphQL execution

createGraphqlApplication() executes operations via Apollo's executeOperation, with the framework's scoped container resolver attached so guards, interceptors, and field resolvers run exactly as they would over HTTP.

Executing a query
const gql = await moduleRef.createGraphqlApplication();const { data, errors } = await gql.execute<{ report: { message: string; score: number } }>({  query: /* GraphQL */ `    query Report($score: Int!) {      report(score: $score) {        message        score      }    }  `,  variables: { score: 7 },});expect(errors).toBeUndefined();expect(data?.report).toEqual({ message: 'live', score: 7 });

Global schema metadata: the GraphQL type registry is a process-wide singleton shared by every test file. If you declare types at module scope, reset it between cases so your suite is independent of run order.

Resetting global GraphQL state
import {  GraphqlMetadataStorage,  clearGraphqlGuards,  clearGraphqlInterceptors,} from '@nl-framework/graphql';beforeEach(() => {  GraphqlMetadataStorage.get().clear();  clearGraphqlGuards();  clearGraphqlInterceptors();});

Microservice handlers

createMicroserviceHarness() drives MessageDispatcher through an in-memory transport — no Dapr sidecar. Use send() for request/response patterns and emit() for events.

Dispatching messages
const harness = await moduleRef.createMicroserviceHarness();// Defaults to the module's discovered controllers.// Pass { controllers: [OrdersController] } to scope it explicitly.// Request/responseconst total = await harness.send<number>('math.double', { value: 21 });expect(total).toBe(42);// Fire-and-forget eventawait harness.emit('math.logged', { value: 5 });

The underlying InMemoryTransport is exported too, so you can wire it into a real module.

Reusing the in-memory transport
import { InMemoryTransport, MicroserviceHarness } from '@nl-framework/testing';import { createMicroservicesModule } from '@nl-framework/microservices';const harness = await moduleRef.createMicroserviceHarness();// Reuse the same in-memory transport inside a module wiring.const module = createMicroservicesModule({ transport: harness.getTransport() });

Repositories

InMemoryRepository<T> is a dependency-free implementation of the full OrmRepository contract — find, findOne, findById, count, insertOne/insertMany, save, updateMany, softDelete/restore, and deleteHard/deleteMany. It mirrors MongoRepository's id, timestamp, and soft-delete semantics with a Mongo-style filter matcher — no real MongoDB and no mongodb-memory-server. Substitute it for a real repository on its injection token.

Substituting a repository
import { InMemoryRepository, createInMemoryRepository } from '@nl-framework/testing';import { getRepositoryToken } from '@nl-framework/orm';// Defaults timestamps + softDelete to true, matching @Document.const repo = new InMemoryRepository(User, {  seed: [{ name: 'Ada', age: 36 }],});const moduleRef = await Test.createTestingModule({ imports: [AppModule] })  .overrideProvider(getRepositoryToken(User))  .useValue(repo)  .compile();const service = await moduleRef.get(UserService);expect(await service.adults()).toHaveLength(1);// Read timestamps/softDelete/collection from the entity's @Document instead:const repo2 = await createInMemoryRepository(User, { seed: [/* ... */] });

Extra conveniences: repo.snapshot(), repo.size, and repo.clear(). The fake does not emit write-notifier events, and @nl-framework/orm stays an optional peer.

A complete example

Putting it together — an HTTP endpoint test that mocks an external payment provider and binds no port.

orders.e2e.test.ts
import { describe, expect, it } from 'bun:test';import { Test } from '@nl-framework/testing';import { AppModule } from '../src/app.module';import { PaymentGateway } from '../src/payments/payment.gateway';describe('POST /orders', () => {  it('creates an order without a real payment provider', async () => {    const moduleRef = await Test.createTestingModule({ imports: [AppModule] })      .overrideProvider(PaymentGateway)      .useValue({ charge: async () => ({ id: 'ch_test', ok: true }) })      .compile();    const app = await moduleRef.createHttpApplication();    const { status, body } = await app.requestJson('/orders', {      method: 'POST',      json: { sku: 'BOOK-1', quantity: 2 },    });    expect(status).toBe(200);    expect(body).toMatchObject({ status: 'paid', chargeId: 'ch_test' });    await moduleRef.close();  });});

What's next?

Review Lifecycle events to understand what close() triggers, or revisit Guards and Interceptors to see what your overrides replace.