Testing and checking
Two tools close the loop after every change: cell check finds mistakes without running the application, and createTestApp() runs it inside the test process.
cell check
npx cell check # exit code 1 when there are errors
npx cell check --json # one JSON document: passed, summary, issues| Check | Finds |
|---|---|
config | Missing entry, unknown -m modes, CELL_* environment variables that match no key |
types | tsc --noEmit of the project, each diagnostic with file, line and TSxxxx code |
decorators | experimentalDecorators / emitDecoratorMetadata missing in tsconfig.json |
di | Loads the backend container without starting it and resolves every binding: CELL_DI_NO_BINDING, CELL_DI_AMBIGUOUS, CELL_DI_CIRCULAR, ... |
Each issue has a code, a file and line when known, and a hint: a one-sentence fix. --skip <check> leaves one out; components can add their own checks (typeorm:entities, for example).
createTestApp()
// test/users.spec.ts
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import { join } from 'node:path';
import { createTestApp, type TestApp } from '@celljs/cli';
describe('users', () => {
let app: TestApp;
beforeAll(async () => {
app = await createTestApp({ root: join(__dirname, '..') });
}, 60000); // the first start compiles the sources
afterAll(() => app?.close()); // stops the application: onStop runs
it('creates a user over HTTP', async () => {
const res = await app.fetch('/users', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Ada' }),
});
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({ name: 'Ada' });
});
it('calls a service directly', async () => {
const { UserService } = await app.import('/src/node/user-service.ts'); // the module the application loaded
expect(await app.get<any>(UserService).count()).toBe(1);
});
});- The backend is loaded from source the way
cell devloads it: nothing is built and no port is opened.fetch(path, init)sends the request through the whole HTTP chain (middleware, security, controllers, error handlers) and returns a standardResponse. app.get(Id)returns a component of the application,app.container.getAll(Id)all of them. The application runs in its own module instances: take classes fromawait app.import('/src/node/user-service.ts')(a package by its name), not from animportof the test file. A class orSymbolthe test imports itself is matched by name, which works for symbols and for classes without injections, but a class with@Value/@Autowiredimported into the test process fails withCELL_DI_NO_BINDING.- The active mode is
test: put test-only configuration incell.test.yml, for example an in-memory database.createTestApp({ mode: ['seed'] })adds modes aftertest. - Startup failures throw a
CellError(CELL_DEV_BACKEND_FAILEDwith the failing file).fetchwithout an HTTP backend throwsCELL_TEST_APP_NO_HTTP.
Run the tests with bun test (the templates' npm test runs bun test ./test). Each test file gets one application; tests in a file share its state, so make them independent of order or reset data in beforeEach.
Example: a test database
# cell.test.yml
typeorm:
ormConfig:
- type: sqljs # in memory, from the sql.js package
synchronize: trueUnit tests without the application
Plain classes can be tested with new, passing collaborators by hand. Components that use property injection need the container: prefer createTestApp() over assembling one yourself.
Debugging a running application
While cell dev runs, POST /_cell/mcp on the same port is an MCP server for coding agents with get_errors (startup errors, failed reloads, 5xx requests, error logs), list_routes, list_bindings, get_config, recent_requests and logs. See Working with coding agents.