| name | testing-property-based-fast-check |
| description | Property-based testing con fast-check: generar miles de inputs aleatorios para validar propiedades invariantes. Integración con Jest, arbitraries custom, model-based testing, detección de race conditions. |
| version | 1.0.0 |
| author | Mastermind |
Property-Based Testing con fast-check
Framework de testing basado en propiedades que genera miles de inputs aleatorios para verificar que ciertas invariantes se mantienen siempre. Inspirado en QuickCheck (Haskell).
¿Qué es y por qué importa?
El testing tradicional usa ejemplos concretos: expect(f(2)).toBe(4). El property-based testing dice: "para TODOS los enteros positivos x, f(x) debe ser >= x". fast-check genera automáticamente miles de inputs aleatorios y verifica la propiedad. Si falla, hace shrinking automático para encontrar el contraejemplo más pequeño posible.
Datos reales: fast-check tiene 5k+ estrellas en GitHub, es usado por Jest, Jasmine, Ramda, fp-ts, io-ts. Encontró bugs reales en Jest y query-string.
Instalación
npm install --save-dev fast-check
Patrón básico: propiedades
const fc = require('fast-check');
describe('string properties', () => {
it('should always contain itself', () => {
fc.assert(
fc.property(fc.string(), (text) => {
return text.includes(text);
}),
);
});
it('should always contain its substrings', () => {
fc.assert(
fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {
return (a + b + c).includes(b);
}),
);
});
});
Arbitraries integrados (los más útiles)
fc.integer()
fc.integer({ min: 0, max: 100 })
fc.float()
fc.double()
fc.bigInt({ min: 0n, max: 1000n })
fc.string()
fc.string({ minLength: 1, maxLength: 50 })
fc.asciiString()
fc.hexaString({ minLength: 6, maxLength: 6 })
fc.fullUnicodeString()
fc.array(fc.integer())
fc.array(fc.string(), { minLength: 1, maxLength: 10 })
fc.uniqueArray(fc.integer(), { minLength: 3, maxLength: 10 })
fc.oneof(fc.string(), fc.integer())
fc.constant('fixed value')
fc.constantFrom('red', 'green', 'blue')
fc.option(fc.string())
fc.nat()
Arbitraries custom con fc.record y fc.dictionary
const userArbitrary = fc.record({
id: fc.integer({ min: 1 }),
name: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.string().filter(s => s.includes('@')),
age: fc.integer({ min: 0, max: 150 }),
});
it('user age should be valid', () => {
fc.assert(
fc.property(userArbitrary, (user) => {
expect(user.age >= 0 && user.age <= 150).toBe(true);
}),
);
});
const stringToNumber = fc.dictionary(fc.string(), fc.integer());
Precondiciones con fc.pre()
it('reverse of reverse is identity (only for non-empty)', () => {
fc.assert(
fc.property(fc.string(), (text) => {
fc.pre(text.length > 0);
return reverse(reverse(text)) === text;
}),
);
});
Gen: composición de arbitraries
const userWithOrders = fc.gen(() => {
const userId = yield* fc.integer({ min: 1 });
const name = yield* fc.string({ maxLength: 30 });
const orderCount = yield* fc.integer({ min: 0, max: 10 });
const orders = yield* fc.array(
fc.record({
id: fc.integer({ min: 1 }),
amount: fc.float({ min: 0.01, max: 10000 }),
}),
{ minLength: orderCount, maxLength: orderCount }
);
return { userId, name, orders };
});
Integración con Jest
const fc = require('fast-check');
describe('convertEsiosValue', () => {
it('should divide by 10 for indicators with factor 10', () => {
fc.assert(
fc.property(fc.integer({ min: 0, max: 100000 }), (raw) => {
const result = convertEsiosValue(raw, 10);
expect(result).toBe(raw / 10);
}),
);
});
it('should handle zero values', () => {
fc.assert(
fc.property(fc.integer(), (raw) => {
const result = convertEsiosValue(0, 10);
expect(result).toBe(0);
}),
);
});
});
Ejemplo práctico: conversión de unidades ESIOS
describe('ESIOS unit conversion properties', () => {
it('conversion should be invertible', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 1000000 }),
fc.constantFrom([1, 10, 100, 1000, 10000]),
(raw, factor) => {
const converted = Math.floor(raw / factor);
const restored = converted * factor;
expect(restored).toBeLessThanOrEqual(raw);
},
),
);
});
it('demand should not exceed supply', () => {
fc.assert(
fc.property(
fc.array(fc.integer({ min: 0, max: 100000 }), { minLength: 1 }),
fc.integer({ min: 0, max: 100000 }),
(sources, demand) => {
const total = sources.reduce((a, b) => a + b, 0);
expect(demand <= total).toBe(true);
},
),
);
});
});
Model-based testing (avanzado)
const counterModel = fc.model(
fc.constantFrom('increment', 'decrement', 'reset'),
{
init: () => ({ value: 0 }),
run: (state, action) => {
switch (action) {
case 'increment': return { ...state, value: state.value + 1 };
case 'decrement': return { ...state, value: state.value - 1 };
case 'reset': return { ...state, value: 0 };
}
},
assert: (state, executed) => {
},
},
{ preconditions: () => true },
);
fc.assert(fc.modelVerify(counterModel));
Detección de race conditions
it('should handle concurrent operations deterministically', () => {
fc.assert(
fc.asyncProperty(
fc.array(fc.integer()),
fc.shuffleArray,
async (items, shuffled) => {
const results = await Promise.all(
items.map(i => processAsync(i))
);
const sorted = [...results].sort();
expect(sorted).toEqual(sorted);
},
),
);
});
Configuración avanzada
fc.assert(property, { numRuns: 1000 });
fc.assert(property, { seed: 12345 });
fc.assert(property, {
customReporter: {
onRunComplete: (completion) => {
console.log(`Tests: ${completion.counterexampleCount === null ? 'passed' : 'failed'}`);
},
},
});
fc.assert(property, { verbose: true });
Cuándo usar
- ✅ Validar invariantes matemáticas (orden, simetría, invertibilidad)
- ✅ Probar funciones de transformación/pure functions
- ✅ Generar edge cases que un humano no pensaría
- ✅ Testing de APIs REST (fuzzing de inputs)
- ✅ Detectar race conditions en código asíncrono
- ✅ Validar parsers y validadores de datos
Cuándo NO usar
- ❌ Testing de UI interactiva (mejor con Cypress/Playwright)
- ❌ Testing de integración de base de datos (mejor con fixtures)
- ❌ Cuando necesitas ejemplos específicos documentales (keep unit tests para eso)
- ❌ Propiedades que dependen de estado externo no determinista
Pitfalls
- ❌ No usar
fc.pre() en exceso — si filtras el 90% de los inputs, fast-check es lento
- ❌ No mezclar
expect() con return — en fast-check puro usa return boolean, en Jest usa expect() dentro de fc.context()
- ❌ Olvidar el shrinking — si un test falla, fast-check busca el contraejemplo mínimo. Si tu propiedad es compleja, el shrinking puede ser lento
- ❌ No fijar seed en CI — la seed aleatoria es buena para CI (detecta más bugs). Solo fija seed para debugging local
- ❌ No usar
fc.string() para URLs/emails — usar fc.webUrl(), fc.asciiStringWithRandomCharacter() con filtro, o arbitraries específicos
Referencias
Skills relacionadas
testing-jest-mocks-api — mocking de APIs externas con Jest
systematic-debugging — metodología de 4 fases para debugging