Appearance
@znight/testing available
Testing utilities for Znight: TestBed builds one provider with its dependencies auto-mocked; TestApp runs the whole application and injects HTTP requests in memory. It reads the generated metadata structurally and depends on no feature package; it works with Vitest, Jest and bun:test, on Node.js and Bun.
sh
npm install -D @znight/testingTestBed
ts
import { TestBed } from '@znight/testing'
const { instance, mocks } = await TestBed.create(UserService, { metadata })
mocks.get(UserRepository).findById.mockResolvedValue({ id: 1, name: 'Ada' })
expect(await instance.findById(1)).toEqual({ id: 1, name: 'Ada' })- The target is built with its real code; its direct dependencies are replaced by mocks.
- Classes, interfaces,
@Providesvalues and plugin-provided types → auto-mocks;@Config()classes → real instances (initializers,@Defaultvalues, then theconfigoption); qualified values (@Inject('apiUrl'), primitives) → theprovidevalue, an error otherwise; optional →undefined; arrays →[];Lazy<T>→ a handle over the mock. - Options:
metadata,provide,overrides(values or{ scope, deps, factory }definitions),config,mockFn,deep,aop. Keys are classes, tokens, aliases or unique display names. - Interceptors are not applied unless
aop: 'real'. Lifecycle hooks are never called. mocks.get()accepts classes, tokens, aliases and display names (mocks.get<Logger>('Logger'));mocks.clearAll()/mocks.resetAll().- Auto-mocks are not thenable (
thenisundefined), so returning one from an async function is safe; with a class,instanceofworks.createMock(Class?, partial?)creates one yourself.
Spy implementation
The spy factory is detected automatically: vi.fn (Vitest), jest.fn (Jest) or mock (bun:test), falling back to a built-in Jest-compatible spy. Pass mockFn to choose explicitly.
TestApp
ts
import { HttpPlugin } from '@znight/http'
import { TestApp } from '@znight/testing'
await using app = await TestApp.create({
metadata,
plugins: [HttpPlugin.configure({ port: 0 })], // any adapter; port 0: a free port
overrides: [[UserRepository, fakeRepo]],
})
const res = await app.inject({ url: '/users/1' }) // method: GET by default
expect(res.status).toBe(200)
expect(res.json()).toEqual({ id: 1, name: 'Ada' })
const form = new FormData()
form.append('avatar', new File([png], 'me.png', { type: 'image/png' }))
const upload = await app.inject({ method: 'POST', url: '/profile/avatar', body: form })
const download = await app.inject({ url: '/reports/q1.pdf' })
download.bytes // Uint8Array: the raw bodyTestApp.create({ metadata, plugins?, providers?, overrides?, only?, start?, logger?, shutdown? })runsApplication.create()thenstart()(unlessstart: false); signal handlers are disabled.overridesare applied last (highest precedence).- Only the dependency closure of controllers (with their guards and interceptors), consumers, the hosts of scheduled jobs, overridden providers and the providers plugins resolve at bootstrap (
HttpPlugin's global guards, interceptors and exception mappers, listed by the plugin'sinitRoots(context)) is instantiated by default (only: 'all'or a list of keys otherwise). The job hosts are read frommetadata.extensions.schedule; without any controller, consumer or job, every provider is created. inject({ method?, url, headers?, body?, query?, cookies?, signedCookies? })goes through the HTTP plugin in memory (no network call) and resolves with{ status, headers, body, bytes, cookies, json() }. Bodies: objects as JSON, strings and bytes as is,FormDataasmultipart/form-data,Blob/Filewith its type.cookiesare sent in acookieheader,signedCookiesare signed by the plugin first;response.cookiesholds the parsedset-cookieheaders by name ({}without any).injectSse(request)opens an event stream (@Sseroutes,HttpResponse.sse()) in memory and reads it likeEventSource:status,headersandcookiesonce it opened (at its first chunk, or after 2 000 ms for a stream that sends nothing yet; it rejects, naming the request, when no response headers arrive within 2 000 ms), thennext(),take(n)orfor awaitfor the events (each withevent,data,idandjson()),reconnect()withlast-event-id, andclose()(orawait using) to disconnect.injectStream(request)resolves at the response headers with the body as aReadableStream; both accept asignal.inject()rejects an event stream instead of waiting for its end.close()(orawait using) stops the application;get(key),containerandappgive access to it.
ts
await using stream = await app.injectSse({ url: '/orders/o-1/events' })
hub.publish({ orderId: 'o-1', sequence: 42, type: 'status', status: 'paid' })
expect(await stream.next()).toMatchObject({ event: 'status', id: '42' })A plugin named 'http' that does not implement injectSse() / injectStream() (another plugin than HttpPlugin registered under that name) fails with ZNIGHT001.
Keeping metadata fresh
Tests import the generated container, so regenerate it first: the @znight/compiler/bun preload for bun test, the Vite plugin under Vitest, or znight generate otherwise. See Testing.