Testing
TestCrudFactory builds mock db, table, and entity instances for fast unit tests: no real database required.
The package ships a TestCrudFactory with helpers to construct a working service backed by an in-memory Drizzle instance. The result is a fast unit test that exercises the real base class, findAll, create, hooks, transactions, the lot, without spinning up Postgres or MySQL.
The four helpers
import { TestCrudFactory, SqlBaseCrudService } from 'nestjs-drizzle-crud';
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';
const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull().$onUpdate(() => new Date()),
deletedAt: timestamp('deleted_at'),
});
type User = typeof users.$inferSelect;
class UsersService extends SqlBaseCrudService<User, User, Partial<User>, Partial<User>> {}
export const buildUsersServiceFixture = () => {
const mockDb = TestCrudFactory.createMockDb();
const mockTable = TestCrudFactory.createMockTable(users);
return TestCrudFactory.createTestService(UsersService, mockDb, mockTable, {
primaryKey: 'id',
primaryKeyType: 'serial',
softDelete: { enabled: true, column: 'deleted_at' },
});
};| Method | Returns | Notes |
|---|---|---|
createMockDb() | Stub db | Jest mock functions for the subset of the Drizzle API the package calls. Runs no SQL. |
createMockTable() | Stub table | A column-name map (id, name, email, created_at, updated_at, deleted_at). Takes no arguments. |
createMockEntity(overrides?) | Row fixture | A row with sensible defaults, merged with your overrides. |
createTestService(Service, db, table, config?) | T | Constructs the service with the merged config. |
A complete test
import { Test } from '@nestjs/testing';
import {
TestCrudFactory,
EntityNotFoundException,
ValidationFailedException,
} from 'nestjs-drizzle-crud';
import { users } from '../db/schema';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const mockDb = TestCrudFactory.createMockDb();
const mockTable = TestCrudFactory.createMockTable(users);
service = TestCrudFactory.createTestService(UsersService, mockDb, mockTable, {
primaryKey: 'id',
primaryKeyType: 'serial',
softDelete: { enabled: true, column: 'deleted_at' },
});
// For Nest DI in service constructors, set up a TestingModule.
// For empty-subclass services this isn't needed.
});
it('creates and finds a user', async () => {
const user = await service.create({ name: 'Ada', email: 'ada@example.com' });
expect(user.name).toBe('Ada');
const found = await service.find(user.id);
expect(found?.email).toBe('ada@example.com');
});
it('throws on update of missing row', async () => {
await expect(service.update(999, { name: 'X' })).rejects.toBeInstanceOf(
EntityNotFoundException,
);
});
it('excludes soft-deleted rows from findAll', async () => {
const u = await service.create({ name: 'A', email: 'ada@example.com' });
await service.softDelete(u.id);
const { data, total } = await service.findAll({});
expect(total).toBe(0);
});
});When you need real Nest DI
If your service has its own constructor-injected dependencies (event bus, logger, other services), wrap the service in a Nest TestingModule:
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: 'EVENT_BUS', useValue: { emit: jest.fn() } },
],
}).compile();
service = module.get(UsersService);
});The forFeature registration isn't needed in tests because TestCrudFactory.createTestService constructs the service directly.
Testing hooks
Hooks are just methods on the service. The simplest way to verify they fire is to spy on them:
it('calls beforeCreate with the input', async () => {
const spy = jest.spyOn(service as any, 'beforeCreate');
await service.create({ name: 'Ada', email: 'ada@example.com' });
expect(spy).toHaveBeenCalledWith({ name: 'Ada', email: 'ada@example.com' });
});Or override a hook in a test-only subclass:
class TestUsersService extends UsersService {
protected async beforeCreate(data: CreateUserDto) {
return { ...data, slug: 'test-slug' };
}
}What createMockDb actually does
createMockDb() returns a hand-built stub whose methods are Jest mock functions (select, insert, update, delete, and the chainable from / where / limit / offset / orderBy / returning). It runs no SQL. You decide what a query resolves to by setting the mock's return value, which is what makes these tests fast and deterministic.
Two consequences to plan for:
- It needs a Jest environment. The factory calls the global
jest, so it works inside a Jest test file and nowhere else. - It does not verify your SQL. A stub cannot catch a wrong column name, a bad cast, or a constraint violation. Cover those with an end-to-end test against a real database, the way this project's own
apps/apie2e suite does: Playwright drives the app over HTTP while the app runs against PGlite, Postgres compiled to WASM, in-process. That needs no container and no credentials, so there is no excuse for skipping the SQL-level test.
createMockTable() returns a plain object whose keys stand in for columns (id, name, email, created_at, updated_at, deleted_at), and createMockEntity(overrides?) returns a row fixture you can override field by field.
If you want tests that exercise real SQL without a server, point createTestService at your own in-memory Drizzle instance, for example pglite, instead of the stub. The factory takes any db you hand it.