Testing Overview
Testing is a first-class citizen in Carno.js. We provide utilities to make it easy to write unit and integration tests for your applications.
Testing with the Database
When writing integration tests that involve the ORM, you need a clean database state for each test. The withDatabase helper simplifies this process by handling connection management, schema creation, and cleanup.
Basic Usage
The withDatabase helper creates a temporary schema, runs your migrations (or provided SQL statements), and executes your test routine within an ORM session.
import { withDatabase } from '@carno.js/orm/testing';
import { User } from '../entities/User';
describe('UserRepository', () => {
it('should create a user', async () => {
await withDatabase(async ({ orm }) => {
const user = await User.create({
name: 'John Doe',
email: 'john@example.com'
});
expect(user.id).toBeDefined();
});
});
});
Providing SQL Statements
You can provide an array of SQL statements to initialize the database schema manually if you don't want to rely on migrations.
await withDatabase(
[
'CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT)'
],
async ({ orm, executeSql }) => {
// Your test logic here
await executeSql("INSERT INTO users (name) VALUES ('Jane')");
}
);
Options
withDatabase accepts an options object to customize the connection and behavior.
await withDatabase(
async ({ orm }) => { /* ... */ },
{
schema: 'test_schema', // Custom schema name (default: public)
entityFile: 'src/**/*.entity.ts', // Glob pattern to load entities
connection: {
host: 'localhost',
// ... overrides for ConnectionSettings
}
}
);
How it Works
- Schema Isolation: For every call, it drops and recreates the specified schema (defaulting to
public). - Auto-Migrations: If
migrationPathis found in yourcarno.config.ts(or provided in options), it automatically loads and executes the SQL migration files. - Session Context: It wraps the execution in an
ormSessionContext, so that Active Record methods and Repositories use the correctOrminstance. - Cleanup: Since it recreates the schema every time, each test starts with a completely fresh database.
Testing Controllers
Carno ships a test harness for controller and HTTP testing. It creates a Carno
instance, registers controllers/services, and can start a real HTTP server so you
can call routes using fetch.
Quick start with automatic cleanup
Use withTestApp when you want a small integration test and automatic cleanup.
It always calls close() even if your test throws.
import { withTestApp, Controller, Get } from '@carno.js/core';
@Controller('/health')
class HealthController {
@Get()
health() {
return { status: 'ok' };
}
}
await withTestApp(
async (harness) => {
const response = await harness.get('/health');
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ status: 'ok' });
},
{
controllers: [HealthController],
listen: true
}
);
Manual lifecycle control
Use createTestHarness when you need to keep the server open for multiple
assertions or want to set up extra state before closing.
import { createTestHarness } from '@carno.js/core';
const harness = await createTestHarness({
controllers: [HealthController],
services: [],
listen: true
});
try {
const response = await harness.get('/health');
expect(response.status).toBe(200);
} finally {
await harness.close();
}
Harness API overview
app: TheCarnoinstance you can configure directly.container: The internal DI container.serverandport: Defined only whenlistenis enabled.resolve(token): Resolve a service from the container.request,get,post,put,delete: HTTP helpers.postandputserialize JSON and setContent-Type: application/json.close(): Stops the server and cleans up.
Options
controllers: Controllers to register on the test app.services: Services or tokens to register in the DI container.config:CarnoConfigoverrides for the test instance.listen:trueto start a server on a random port, or a number to bind a specific port.port: Alternate way to set a specific port (used whenlistenistrue).
Notes
- If you do not set
listen, the server will not start and HTTP helpers (request,get,post,put,delete) will throw. requestaccepts either a full URL or a path like/health.