Skip to main content

Testing Workflows

Because DBOS is a regular Java object injected into your workflow classes — not a global static — it can be mocked with any standard Java mocking library such as Mockito. This lets you test your workflow logic in complete isolation, without a PostgreSQL database.

Unit Testing​

The key insight is that dbos.runStep(lambda, name) takes a lambda that is only executed by the real DBOS runtime. When DBOS is mocked, the lambda is never called — you control what the mock returns. This lets you test workflow branching logic (payment succeeds, inventory is low, etc.) by stubbing step outcomes directly.

Setup​

Construct your workflow class directly, passing a mock DBOS:

import static org.mockito.Mockito.*;

class CheckoutWorkflowTest {

// Mockito struggles with generic ThrowingSupplier/ThrowingRunnable types,
// so define typed helpers to avoid unchecked-cast warnings.
private static ThrowingRunnable<RuntimeException> anyRunnable() {
return ArgumentMatchers.any();
}

private static <T> ThrowingSupplier<T, RuntimeException> anySupplier() {
return ArgumentMatchers.any();
}

private DBOS mockDBOS;
private WidgetStoreRepository mockRepo;
private WidgetStoreService mockSelf;
private WidgetStoreService service;

@BeforeEach
void setUp() {
mockDBOS = mock(DBOS.class);
mockRepo = mock(WidgetStoreRepository.class);
mockSelf = mock(WidgetStoreService.class);
service = new WidgetStoreService(mockDBOS, mockRepo);
service.setSelf(mockSelf); // inject the mocked self-proxy
}
}

mockRepo and mockSelf are mocked separately. Because the workflow body calls them only via dbos.runStep(() -> repo.someMethod(), "name"), the lambdas are never executed against the mocks — but you can verify that the steps were invoked in the right order with the right names.

Writing Tests​

Stub step return values and verify the workflow's sequence of DBOS calls:

@Test
void checkoutWorkflow_paymentSuccessful_paysAndDispatchesOrder() throws Exception {
int orderId = 42;
when(mockDBOS.runStep(anySupplier(), eq("createOrder"))).thenReturn(orderId);
when(mockDBOS.recv(eq(PAYMENT_STATUS), any())).thenReturn(Optional.of("paid"));

service.checkoutWorkflow();

InOrder inOrder = Mockito.inOrder(mockDBOS);
inOrder.verify(mockDBOS).runStep(anyRunnable(), eq("subtractInventory"));
inOrder.verify(mockDBOS).runStep(anySupplier(), eq("createOrder"));
inOrder.verify(mockDBOS).setEvent(eq(PAYMENT_ID), any());
inOrder.verify(mockDBOS).recv(eq(PAYMENT_STATUS), any());
inOrder.verify(mockDBOS).runStep(anyRunnable(), eq("markOrderPaid"));
inOrder.verify(mockDBOS).startWorkflow(anyRunnable());
inOrder.verify(mockDBOS).setEvent(eq(ORDER_ID), eq(String.valueOf(orderId)));
}

@Test
void checkoutWorkflow_insufficientInventory_setsNullPaymentIdAndReturns() throws Exception {
doThrow(new RuntimeException("Insufficient Inventory"))
.when(mockDBOS)
.runStep(anyRunnable(), eq("subtractInventory"));

service.checkoutWorkflow();

verify(mockDBOS).setEvent(eq(PAYMENT_ID), eq(null));
verify(mockDBOS, never()).runStep(anySupplier(), eq("createOrder"));
}
tip

Add an @AfterEach assertion that mockRepo and mockSelf had no direct interactions. This guards against workflow code accidentally calling them outside a runStep wrapper:

@AfterEach
void verifyNoDirectCalls() {
verifyNoInteractions(mockRepo, mockSelf);
}

Spring Boot​

With the Spring Boot starter, DBOS is injected by Spring and the workflow class is a @Service. You can test it the same way — construct the service directly with mocks rather than loading the Spring context:

@BeforeEach
void setUp() {
mockDBOS = mock(DBOS.class);
service = new MyWorkflowService(mockDBOS, otherDependencies...);
}

This is faster than @SpringBootTest and keeps the test free of container overhead.

Integration Testing​

For tests that exercise real durable execution — recovery, exactly-once steps, queues — you need a live PostgreSQL database and a real DBOS instance.

DBOS implements AutoCloseable, so use try-with-resources to guarantee shutdown:

@Test
void workflow_resumesAfterInterruption() throws Exception {
DBOSConfig config = DBOSConfig.defaults("test-app")
.withAppVersion("0.1.0")
.withDatabaseUrl(System.getenv("DBOS_TEST_JDBC_URL"))
.withDbUser(System.getenv("PGUSER"))
.withDbPassword(System.getenv("PGPASSWORD"));

try (var dbos = new DBOS(config)) {
var workflows = dbos.registerProxy(MyWorkflows.class, new MyWorkflowsImpl(dbos));
dbos.launch();
// exercise real durable behaviour
workflows.myWorkflow("input");
}
}

For test isolation, use a fresh database per test run. The easiest approach is Testcontainers with the PostgreSQL module, which spins up a throwaway PostgreSQL container:

@Testcontainers
class MyWorkflowIntegrationTest {

@Container
static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:latest");

@Test
void myWorkflow_completesSuccessfully() throws Exception {
DBOSConfig config = DBOSConfig.defaults("test-app")
.withAppVersion("0.1.0")
.withDatabaseUrl(postgres.getJdbcUrl())
.withDbUser(postgres.getUsername())
.withDbPassword(postgres.getPassword());

try (var dbos = new DBOS(config)) {
var workflows = dbos.registerProxy(MyWorkflows.class, new MyWorkflowsImpl(dbos));
dbos.launch();
workflows.myWorkflow("input");
}
}
}