Stone.jsDocs
Paradigm

Extensions

Testing

A test is only worth its resemblance to production. @stone-js/testing boots the actual application in memory and sends it real intentions through the same kernel production uses, so you assert on behaviour a caller would see, not on the shape of your mocks.

#Install

terminalbash
npm i -D @stone-js/testing

The package itself knows about no platform: the main entry imports neither an HTTP package nor a browser one. The event factories sit behind subpaths, so a native application, a CLI one or a worker installs nothing it does not use.

EntryTypeDescription
@stone-js/testingalwayscreateTestApp, the client, container substitutions, and makeIncomingEvent for a platform-agnostic cause.
@stone-js/testing/httpneeds @stone-js/http-coremakeIncomingHttpEvent, for an application served over HTTP.
@stone-js/testing/browserneeds @stone-js/browser-coremakeIncomingBrowserEvent, for a browser or a React Native application.

#Behaviour over mocks

The principle

Tests built from mocks verify that your code calls your mocks. They pass while the system is broken and break while it is fine. A test that drives the real boundary verifies what actually happens.

In Stone.js

createTestApp boots your real app on an in-memory adapter and returns a client. app.send(makeIncomingHttpEvent(...)) dispatches an event through the full kernel, and you assert on the returned response. No HTTP, no network, full fidelity.

tests/tasks.test.tsts
import { createTestApp } from '@stone-js/testing'
import { makeIncomingHttpEvent } from '@stone-js/testing/http'

it('creates a task', async () => {
  // No module list: your app is discovered from app/**, the same files the CLI builds.
  const app = await createTestApp()

  const response = await app.send(makeIncomingHttpEvent({
    method: 'POST',
    url: '/tasks',
    body: { title: 'Ship the docs' }
  }))

  expect(response.statusCode).toBe(201)
  expect(response.json()).toMatchObject({ title: 'Ship the docs' })
})

#Frontend apps answer with a page

A rendered page is an HTML string, so it is asserted like any other response. There is no assertion library here on purpose: query that HTML with whatever you already use (happy-dom, jsdom, Testing Library).

tests/home.test.tsts
const response = await app.send(makeIncomingHttpEvent({ url: '/' }))

expect(response.html()).toContain('<h1>Tasks</h1>')

#Substituting a dependency

A fake repository, a fixed clock, a provider made to fail: bindings substitutes container registrations after your own, in the container the kernel builds for each event, so the code under test resolves the fake exactly as it resolves the real one.

tests/expiry.test.tsts
const app = await createTestApp({
  bindings: { clock: { now: () => '2026-01-01T00:00:00.000Z' } }
})

#One config file, tests included

stone test runs your suite with Vitest, configured from stone.config.mjs like the build is. It does two things a bare runner cannot: it loads .env.test before the runner starts, so a value read at module load sees it, and it hands the test process the same file set the build uses, so a suite cannot boot a different application than the one that ships.

stone.config.mjsjs
export default defineBuilderConfig({
  test: {
    envFile: '.env.test',                  // loaded before anything imports
    include: ['./tests/**/*.spec.ts'],
    vitest: { environment: 'happy-dom' }   // raw Vitest config, merged over the defaults
  }
})
Boot the real app. Send a real intention. Assert on the real response.

#The harness API

APITypeDescription
createTestApp(options?)(opts) => Promise<TestClient>Boot the app in memory, discovering modules from app/** unless options.modules names them.
options.appDir / patternstringWhere to discover from, for a non-standard layout.
options.envFilestring | falseEnv file to load before booting. Defaults to .env.test; a missing file is not an error.
options.bindingsRecord<string, unknown>Container substitutions by alias, bound after the app's own registrations.
app.send(event)(event) => Promise<Response>Dispatch an event through the full kernel.
options.blueprintPartial<StoneBlueprint>Configuration to force, merged after the app's own modules so it wins. The counterpart of bindings: one replaces a service, the other replaces a value.
options.platformstringThe context to run as, when an app stacks several. A browser or native renderer registers itself against it.
makeIncomingHttpEvent(opts)(opts) => eventFrom /http. Build an HTTP event: { method, url, body, headers, ip }.
makeIncomingBrowserEvent(opts)(opts) => eventFrom /browser. Build the event a browser or native app receives: { url, metadata }. Keeps your own scheme, so myapp://tasks/42 reaches the route a phone reaches.
response.statusCodenumberThe response status.
response.json()<T>() => TThe body as data: parsed when the payload is a JSON string.
response.html() / text()() => stringThe body as text, for a rendered page.

#Every context, one harness

Because the harness dispatches intentions through the kernel, the same test covers the behaviour whether the app will finally run on Node, on the edge, or as agent tools. You test the domain once; the contexts do not change what it does.

HTTP is not the only cause. makeIncomingHttpEvent builds an HTTP intention; makeIncomingEvent builds a generic one, so the same app.send()exercises a CLI command or an agent tool call, no server and no argv parsing required.

An application that renders needs its own: a browser and a phone receive an IncomingBrowserEvent, and the React renderer keys its hydration snapshot on that event's identity. Name the platform, send the event that platform delivers, and a native application is tested exactly like a web one.

tests/TaskScreen.spec.tsts
import { createTestApp } from '@stone-js/testing'
import { makeIncomingBrowserEvent } from '@stone-js/testing/browser'
import { REACT_NATIVE_PLATFORM } from '@stone-js/react-native-adapter'

const app = await createTestApp({ platform: REACT_NATIVE_PLATFORM })
const response = await app.send(makeIncomingBrowserEvent({ url: 'myapp://tasks/42' }))

expect(response.statusCode).toBe(200)
tests/prune.test.tsts
import { createTestApp, makeIncomingEvent } from '@stone-js/testing'

const app = await createTestApp({ modules: [PruneCommand, TaskService] })
const res = await app.send(makeIncomingEvent({ name: 'tasks:prune', days: 30 }))
expect(res.getContent()).toContain('Pruned')

Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)