Testing

Run workflows end to end in-memory and assert on their behavior with Skipper's test helpers.

You test actions like any other code. Workflows are tested with Skipper’s helpers, which run a workflow end to end against an in-memory backend and let you drive it through its states.

Test setup

Tests run against Skipper’s default in-memory store, so there is no backend to configure — extend the test base and write your test.

class OrderWorkflowTest : SkipperTest() { /* ... */ }
public class OrderWorkflowTest extends SkipperTest { /* ... */ }

The test harness is being finalized for the public release; the exact base class may change, but the helpers below are the stable surface.

A basic test

Build the workflow (the helper generates a unique id), drive it, wait for the state you expect, then assert. Don’t block on the result while the workflow may still be running — use the wait helpers instead.

@Test
fun happyPath() = runBlocking {
  val workflow = workflowBuilder<OrderWorkflow>().build()

  workflow.processOrder(OrderRequest("cust1", 100))
  helper.waitForWorkflowToComplete()

  val result = workflow.processOrder(OrderRequest("cust1", 100)) // safe after completion
  assertEquals("completed", result.status)
}
@Test
public void happyPath() {
  OrderWorkflow workflow = workflowBuilder(OrderWorkflow.class).build();

  workflow.processOrder(new OrderRequest("cust1", 100));
  helper.waitForWorkflowToComplete();

  OrderResult result = workflow.processOrder(new OrderRequest("cust1", 100)); // safe after completion
  assertEquals("completed", result.getStatus());
}

Useful helpers include waitForWorkflowToComplete(), waitForWorkflowToReachStatus(...), expectWorkflowToWait(), and printEvents() for debugging.

Testing waits and signals

Drive a workflow to a wait, assert it’s waiting, send the signal, then let it finish.

@Test
fun requiresApproval() = runBlocking {
  val workflow = workflowBuilder<TransferWorkflow>().build()
  workflow.transfer(TransferRequest("Mary", "Bob", 1001))

  helper.expectWorkflowToWait()   // amount > 1000 requires approval
  workflow.approve(true)          // signal
  helper.waitForWorkflowToComplete()
}
@Test
public void requiresApproval() {
  TransferWorkflow workflow = workflowBuilder(TransferWorkflow.class).build();
  workflow.transfer(new TransferRequest("Mary", "Bob", 1001));

  helper.expectWorkflowToWait();  // amount > 1000 requires approval
  workflow.approve(true);         // signal
  helper.waitForWorkflowToComplete();
}

Testing compensation

Make a later action fail and assert that earlier actions were compensated.

@Test
fun compensatesOnFailure() = runBlocking {
  whenever(shippingService.schedule(any())).thenThrow(RuntimeException("Shipping failed"))

  val workflow = workflowBuilder<OrderWorkflow>().build()
  workflow.processOrder(orderRequest)

  helper.waitForWorkflowToReachStatus(Status.COMPENSATION_COMPLETED)
  verify(paymentService).refund(any())
  verify(inventoryService).cancelReservation(any())
}
@Test
public void compensatesOnFailure() {
  when(shippingService.schedule(any())).thenThrow(new RuntimeException("Shipping failed"));

  OrderWorkflow workflow = workflowBuilder(OrderWorkflow.class).build();
  workflow.processOrder(orderRequest);

  helper.waitForWorkflowToReachStatus(Status.COMPENSATION_COMPLETED);
  verify(paymentService).refund(any());
  verify(inventoryService).cancelReservation(any());
}

Mocking dependencies

Action dependencies (the services your actions call) are mocked normally — bind a mock in the test:

@Bind private val paymentService: PaymentService = mock()
@Bind private final PaymentService paymentService = mock();

Note that the actions themselves are not mocked, so a workflow test runs the real workflow-and-action path end to end (an integration test). Mock the collaborators behind your actions to control the scenario.