Property Based Testing
Zeebe usa property-based testing con BPMN models generados aleatoriamente y execution paths aleatorios. Por defecto: 6 procesos × 30 paths = 180 test cases. La propiedad universal: "todo proceso BPMN válido, ejecutado por cualquier path válido, debe completarse y tener tree paths poblados". Detecta bugs en processors raramente ejercitados que tests específicos pueden no cubrir.
La idea fundamental¶
En vez de testear casos específicos (test-by-example), property tests generan inputs aleatorios y verifican que una propiedad universal se mantenga.
Comparación¶
// Test-by-example (típico)
@Test
public void shouldCompleteSimpleProcess() {
deploy(simpleBpmn);
var pid = createInstance(simpleBpmn);
assertCompletes(pid);
}
// Property-based (Zeebe approach)
@Test
@Parameterized
public void shouldExecuteProcessToEnd(TestDataRecord record) { // record = random
record.getBpmnModels().forEach(deployment::withXmlResource);
deployment.deploy();
record.getExecutionPath().getSteps().forEach(processExecutor::applyStep);
// Propiedad: cualquier proceso válido + cualquier path válido debe completar
assertProcessCompletes(record);
assertAllActivatingElementsHaveTreePathPopulated(record.getProcessId());
}
Configuración en Zeebe¶
private static final String PROCESS_COUNT = System.getProperty("processCount", "6");
private static final String EXECUTION_PATH_COUNT = System.getProperty("executionCount", "30");
Defaults: 6 procesos × 30 paths = 180 test cases por run.
Comentario del código:
"With 10 processes and 100 paths there is a theoretical maximum of 1000 records. However, in tests the number of actual records was around 300, which can execute in about 1 m."
Conclusión: scaling más allá de 6×30 da retornos decrecientes porque pocos procesos tienen 100 paths únicos.
Las dos propiedades testeadas¶
1. ProcessExecutionRandomizedPropertyTest¶
Propiedad: ejecución completa
Plus: todos los activating elements deben tener tree paths poblados.
2. ReplayStateRandomizedPropertyTest¶
Propiedad: replay determinism
∀ valid_bpmn, ∀ valid_path:
processing_state(bpmn, path) = replay_state(log_of(processing(bpmn, path)))
Ver concepts/replay-determinism para detalles.
Generación de procesos¶
TestDataGenerator.generateTestRecords(processCount, pathCount) genera:
- BPMN models aleatorios con elementos válidos combinados aleatoriamente
- Execution paths aleatorios dentro de cada model (cómo se va a ejecutar)
Sin código del generator a la vista, pero por el nombre se infiere: - Bloques de elementos (tasks, gateways, events) - Profundidad limitada - Branching limitado (para evitar explosión exponencial)
Reproducibilidad de failures¶
Cuando un property test falla, no es obvio cómo reproducir el bug — el input fue aleatorio.
Solución: FailedPropertyBasedTestDataPrinter
@Rule
public TestWatcher failedTestDataPrinter =
new FailedPropertyBasedTestDataPrinter(this::getDataRecord);
JUnit TestWatcher que se activa en failure y imprime los datos del test (BPMN XML + execution path) para reproducción manual.
Hay también un mecanismo para re-ejecutar un test específico con seed conocido:
// (comentado en el código pero disponible)
return List.of(
TestDataGenerator.regenerateTestRecord(-8532388551768899121L, -6565756334590616537L));
Dos seeds (long) deterministicamente reproducen un test case específico.
Ventajas vs test-by-example¶
| Aspecto | Test-by-example | Property-based |
|---|---|---|
| Coverage | Casos específicos elegidos | Espacio de inputs (sample) |
| Bugs sutiles | Difíciles de anticipar | Encontrados frecuentemente |
| Mantenimiento | Tests rotos al refactorizar | Propiedades estables |
| Documentación | Casos individuales | Invariantes universales |
| Costo | Bajo por test | Alto por test (más tiempo) |
Limitaciones¶
- No reemplaza tests específicos: bugs conocidos deben tener regression tests con casos exactos
- Random seed dependency: un bug intermitente solo se encuentra si el seed lo expone
- Lentos: 180 tests × ~300 records cada uno = minutos de ejecución
- Difíciles de debug: input aleatorio requiere herramientas para capturarlo
Implicaciones para el MVP¶
Tests que el MVP debería tener¶
Property tests prioritarios:
- Process completion: cualquier proceso válido + path válido → completes
- Replay determinism: processing state = replay state
- State consistency: variable counts, element counts coherentes después de execution
- Idempotency: re-procesar el mismo command (mismo position) no cambia state
Frameworks recomendados: - Java: jqwik, junit-quickcheck - Python: Hypothesis - TypeScript: fast-check - Rust: proptest, quickcheck
Pattern de implementación¶
# Pseudo-code para el MVP
@given(
bpmn_model=valid_bpmn_models(max_elements=10, max_depth=4),
execution_path=valid_paths()
)
def test_process_completes(bpmn_model, execution_path):
engine.deploy(bpmn_model)
pid = engine.create_instance(bpmn_model.process_id)
for step in execution_path.steps:
execute_step(step, pid)
assert process_completed(pid), f"Failed for {bpmn_model}, {execution_path}"
Trade-off¶
Property-based testing requiere construir: - Un generator de BPMN models válidos (no trivial — debe respetar BPMN semantics) - Un generator de execution paths (debe ser consistente con el model) - Helpers para reproducción de failures
Esto es inversión inicial significativa. Para un MVP minimalista, empezar con test-by-example + replay determinism test. Agregar property-based testing cuando el modelo BPMN soportado se estabilice.