Integración con Jest
Los matchers de QuickModel usan el mismo contrato { pass, message() } que expect.extend() de Jest. Sin adaptadores adicionales — quickmodelMatchers funciona directamente en cualquier proyecto Jest.
Instalación
npm install quickmodelConfiguración
Extiende el expect de Jest en tu setup file:
// jest.setup.ts
import { expect } from '@jest/globals';
import { quickmodelMatchers } from 'quickmodel/matchers';
expect.extend(quickmodelMatchers);// jest.config.ts
export default {
setupFilesAfterFramework: ['./jest.setup.ts'],
};Augmentación de TypeScript
Añade un fichero de declaración para que TypeScript reconozca los nuevos matchers:
// types/jest-extended.d.ts
declare namespace jest {
interface Matchers<R> {
toBeValidQModel(): R;
toHaveQRuleError(field: string, message?: string): R;
toHaveQField(fieldName: string): R;
toMatchQModel(expected: object): R;
toBeIntact(): R;
toHaveDirtyField(field: string): R;
}
}Matchers disponibles
toBeValidQModel()
Comprueba que la instancia pasa todas las validaciones @QRule.
const dto = new UserDto({
id: '1',
username: 'alice',
email: 'alice@example.com',
});
expect(dto).toBeValidQModel();
expect(invalidDto).not.toBeValidQModel();toHaveQRuleError(field, message?)
Comprueba que qCheckRules() produce un error en el campo especificado. Opcionalmente verifica el mensaje de error (coincidencia parcial o total).
const dto = new UserDto({ username: 'x', email: 'no-es-email' });
expect(dto).toHaveQRuleError('username'); // cualquier error
expect(dto).toHaveQRuleError('email', 'Email inválido'); // con mensaje
expect(dto).not.toHaveQRuleError('id'); // sin errorestoHaveQField(fieldName)
Comprueba que una propiedad tiene el decorador @QField (recorre la cadena de prototipos).
expect(dto).toHaveQField('email');
expect(dto).not.toHaveQField('internalSecret');toMatchQModel(expected)
Igualdad profunda usando $qSerialize() — incluye valores @QComputed.
const updated = original.$qCopy({ username: 'bob' });
expect(updated).not.toMatchQModel(original);toBeIntact()
Comprueba que hasIntegrity() devuelve true. Una instancia recién creada siempre es íntegra.
expect(new UserDto({ ... })).toBeIntact();toHaveDirtyField(field)
Comprueba que isDirty(field) devuelve true — el campo fue mutado desde la creación.
dto.username = 'bob';
expect(dto).toHaveDirtyField('username');
expect(dto).not.toHaveDirtyField('email');Ejemplo completo
// tests/order.test.ts
import { describe, test, expect, beforeEach } from '@jest/globals';
import { QModel, Quick, QRule, QField, QComputed } from 'quickmodel';
@Quick(
{
orderId: 'string',
amount: 'number',
currency: 'string',
createdAt: Date,
tags: Set,
},
{ unknownPropertyPolicy: 'strip' }
)
class OrderDto extends QModel<{
orderId: string;
amount: number;
currency: string;
createdAt: Date;
tags: Set<string>;
}> {
@QField({ label: 'Importe', required: true })
@QRule((v: number) => v > 0, 'El importe debe ser positivo')
@QRule((v: number) => v <= 1_000_000, 'Importe excede el límite')
declare amount: number;
@QField({ label: 'Moneda', required: true })
@QRule(
(v: string) => /^[A-Z]{3}$/.test(v),
'La moneda debe ser un código ISO de 3 letras'
)
declare currency: string;
@QField({ label: 'ID de pedido', required: true })
@QRule((v: string) => v.length > 0, 'El ID no puede estar vacío')
declare orderId: string;
declare createdAt: Date;
declare tags: Set<string>;
@QComputed()
get resumen() {
return `${this.orderId}: ${this.amount} ${this.currency}`;
}
}
describe('OrderDto', () => {
let pedidoValido: OrderDto;
beforeEach(() => {
pedidoValido = new OrderDto({
orderId: 'PED-001',
amount: 99.99,
currency: 'EUR',
createdAt: '2025-01-15T10:00:00Z',
tags: ['urgente'],
});
});
test('pedido válido pasa todas las reglas', () => {
expect(pedidoValido).toBeValidQModel();
});
test('pedido inválido falla', () => {
const mal = new OrderDto({
orderId: '',
amount: -5,
currency: 'eu',
createdAt: '',
tags: [],
});
expect(mal).not.toBeValidQModel();
expect(mal).toHaveQRuleError('amount', 'El importe debe ser positivo');
expect(mal).toHaveQRuleError(
'currency',
'La moneda debe ser un código ISO de 3 letras'
);
});
test('nuevo pedido es íntegro', () => {
expect(pedidoValido).toBeIntact();
});
test('la mutación es rastreada', () => {
pedidoValido.currency = 'USD';
expect(pedidoValido).toHaveDirtyField('currency');
expect(pedidoValido).not.toHaveDirtyField('amount');
});
test('coerción de Date (string → Date)', () => {
expect(pedidoValido.createdAt).toBeInstanceOf(Date);
});
test('coerción de Set (array → Set)', () => {
expect(pedidoValido.tags).toBeInstanceOf(Set);
});
});Jasmine
Jasmine usa jasmine.addMatchers() con un patrón de factories. Envuelve quickmodelMatchers con el siguiente adaptador:
// test/setup/quickmodel-jasmine.ts
import { quickmodelMatchers } from 'quickmodel/matchers';
type IMatcherFn = (
received: object,
...args: unknown[]
) => { pass: boolean; message: () => string };
function toJasmineMatchers(matchers: Record<string, IMatcherFn>) {
const result: jasmine.CustomMatcherFactories = {};
for (const [name, fn] of Object.entries(matchers)) {
result[name] = () => ({
compare: (actual: object, ...args: unknown[]) => {
const res = fn(actual, ...args);
return { pass: res.pass, message: res.message() };
},
});
}
return result;
}
// En un beforeAll() o fichero de setup de Jasmine:
jasmine.addMatchers(
toJasmineMatchers(quickmodelMatchers as Record<string, IMatcherFn>)
);Luego escribe los tests con naturalidad:
describe('OrderDto — Jasmine', () => {
it('debe ser válido', () => {
expect(new OrderDto({ ... }) as any).toBeValidQModel();
});
it('debe detectar errores de reglas', () => {
const mal = new OrderDto({ orderId: '', amount: -5, currency: 'eu', ... });
expect(mal as any).toHaveQRuleError('amount', 'El importe debe ser positivo');
});
});