Saltar a contenido

Test Infrastructure

El framework de tests del engine Zeebe se basa en 3 abstracciones: EngineRule (JUnit Rule que setupea un engine in-memory), RecordingExporter (mock exporter que captura todos los events para assertions), y fluent builders (BPMN, deployment, processInstance). La API permite que cada test sea conciso y declarativo.

EngineRule — el rule de JUnit

EngineRule es el centro del framework. Setupea un engine completo in-memory:

@Rule
public final EngineRule engineRule = EngineRule.singlePartition();

Configuraciones disponibles:

// Single partition (más común)
EngineRule.singlePartition()

// Multi-partition
EngineRule.multiplePartitions(3)

// Shared storage (para tests de replay)
EngineRule.withSharedStorage(sharedStorage)

// Con modo específico
.withStreamProcessorMode(StreamProcessorMode.REPLAY)
.withStreamProcessorMode(StreamProcessorMode.PROCESSING)

Lo que provee

  • Stream processor inicializado
  • State store in-memory (in-memory RocksDB o equivalente)
  • Writers (StateWriter, CommandWriter, etc.)
  • Exporter mock (RecordingExporter)
  • Time control (clock inyectable)
  • API fluent para acciones

Fluent API para BPMN

Antes de tests, los modelos BPMN se construyen con Bpmn builder:

Bpmn.createExecutableProcess("process")
    .startEvent()
    .serviceTask("task", t -> t.zeebeJobType("type"))
    .exclusiveGateway("decision")
        .condition("=amount > 100", "approve")
        .endEvent("approved")
    .moveToLastGateway()
        .defaultFlow().endEvent("rejected")
    .done()

Cada método retorna un builder fluent. Ventajas vs XML literal: - Type-safe (errores en compile time, no en runtime) - Refactor-friendly - Conciso (10 líneas vs 50 líneas de XML)

Fluent API para deployment

engineRule
    .deployment()
    .withXmlResource(bpmn)
    .deploy();

O con varios resources:

engineRule
    .deployment()
    .withXmlResource(bpmn1)
    .withXmlResource(bpmn2)
    .withDmnResource(dmn)
    .deploy();

Fluent API para process instances

final var processInstanceKey = 
    engineRule.processInstance()
        .ofBpmnProcessId("process")
        .withVariable("amount", 150)
        .create();

Variantes: - .withVariables(map) — múltiples variables - .ofProcessDefinitionKey(key) — por key en vez de bpmnProcessId - .atSpecificStartEvent("eventId") — start instructions - .createWithAwaitingResult() — espera resultado

RecordingExporter — assertions sobre events

Todos los events generados por el engine pasan por RecordingExporter, que los acumula en memoria y provee una API fluent para filtrarlos:

RecordingExporter.processInstanceRecords(ProcessInstanceIntent.ELEMENT_COMPLETED)
    .withProcessInstanceKey(pid)
    .withElementType(BpmnElementType.PROCESS)
    .await();

Filtros disponibles

.withProcessInstanceKey(key)
.withBpmnProcessId("process")
.withElementType(BpmnElementType.SERVICE_TASK)
.withElementId("task1")
.withIntent(ProcessInstanceIntent.ELEMENT_ACTIVATED)
.withValueType(ValueType.PROCESS_INSTANCE)
.withTenantId("tenant1")

Operaciones

.await()           // espera hasta que aparezca el primer match
.limit(n)          // toma los primeros N matches
.findFirst()       // optional con el primero
.count()           // cuenta sin esperar
.toList()          // todos los matches actuales

Multiple records assertion

List<Record<ProcessInstanceRecordValue>> records = 
    RecordingExporter.processInstanceRecords()
        .withProcessInstanceKey(pid)
        .limit(10)
        .asList();

assertThat(records).hasSize(10);
assertThat(records).extracting(Record::getIntent)
    .containsExactly(
        ACTIVATE_ELEMENT, ELEMENT_ACTIVATING, ELEMENT_ACTIVATED, ...
    );

Time control

Clock es inyectable en EngineRule:

engineRule.getClock().pinTime(Instant.parse("2024-01-01T00:00:00Z"));
engineRule.getClock().advanceTime(Duration.ofMinutes(10));

Esto permite testear timers sin tener que esperar realmente:

@Test
public void shouldFireTimerAfter5Minutes() {
    deployTimerProcess();
    createInstance();

    // avanzar el clock virtualmente
    engineRule.getClock().advanceTime(Duration.ofMinutes(5));

    RecordingExporter.timerRecords(TimerIntent.TRIGGERED)
        .await();
}

Job activation

final var activatedJobs = engineRule.jobs()
    .ofType("payment")
    .activate(maxJobs=5);

assertThat(activatedJobs).hasSize(3);

engineRule.job()
    .ofKey(activatedJobs.get(0).getKey())
    .complete(Map.of("result", "approved"));

Test typical pattern

@Test
public void shouldCompleteServiceTask() {
    // GIVEN: deploy un proceso con service task
    engineRule.deployment()
        .withXmlResource(Bpmn.createExecutableProcess("process")
            .startEvent()
            .serviceTask("task", t -> t.zeebeJobType("payment"))
            .endEvent()
            .done())
        .deploy();

    // WHEN: crear instance + completar job
    final var pid = engineRule.processInstance()
        .ofBpmnProcessId("process")
        .create();

    final var job = engineRule.jobs()
        .ofType("payment")
        .activate(1)
        .get(0);

    engineRule.job().ofKey(job.getKey()).complete();

    // THEN: proceso completa
    RecordingExporter.processInstanceRecords(ELEMENT_COMPLETED)
        .withProcessInstanceKey(pid)
        .withElementType(BpmnElementType.PROCESS)
        .await();
}

15 líneas para un test completo end-to-end. La fluent API hace que el código de test sea casi self-documenting.

Implicaciones para el MVP

Inversión inicial: construir el framework de tests

Antes de escribir tests, el MVP necesita:

  1. Engine harness: arrancar el engine in-memory con state limpio
  2. BPMN builder fluent: construir modelos sin XML literal
  3. Recording exporter: capturar events para assertions
  4. Time control: avanzar clock virtualmente
  5. Job/process instance APIs: fluent builders

Esto es ~500-1000 LOC de test infrastructure antes del primer test productivo.

Beneficios a largo plazo

  • Tests cortos y legibles (10-15 líneas cada uno)
  • Refactoring sin romper tests (api fluent es estable)
  • Property-based testing más fácil (random inputs sobre la misma API)
  • Onboarding de nuevos desarrolladores acelerado

Trade-off

Para un MVP minimalista, considerar: - Empezar con tests simples directos contra la API REST - Construir la infrastructure cuando hayan ~30+ tests - Refactorizar tests existentes a la nueva API en batch

Esto evita over-engineering temprano cuando el design del engine aún cambia frecuentemente.