Skip to content

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 ​

bash
npx cell check            # exit code 1 when there are errors
npx cell check --json     # one JSON document: passed, summary, issues
CheckFinds
configMissing entry, unknown -m modes, CELL_* environment variables that match no key
typestsc --noEmit of the project, each diagnostic with file, line and TSxxxx code
decoratorsexperimentalDecorators / emitDecoratorMetadata missing in tsconfig.json
diLoads 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() ​

ts
// 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 dev loads 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 standard Response.
  • 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 from await app.import('/src/node/user-service.ts') (a package by its name), not from an import of the test file. A class or Symbol the test imports itself is matched by name, which works for symbols and for classes without injections, but a class with @Value / @Autowired imported into the test process fails with CELL_DI_NO_BINDING.
  • The active mode is test: put test-only configuration in cell.test.yml, for example an in-memory database. createTestApp({ mode: ['seed'] }) adds modes after test.
  • Startup failures throw a CellError (CELL_DEV_BACKEND_FAILED with the failing file). fetch without an HTTP backend throws CELL_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 ​

yaml
# cell.test.yml
typeorm:
  ormConfig:
    - type: sqljs          # in memory, from the sql.js package
      synchronize: true

Unit 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.