Test and debug workflows
Because Quarkus Flow is deeply integrated with the Quarkus ecosystem, testing your workflows is as straightforward as testing any standard Java application.
This guide shows how to:
-
Write fast unit tests for workflow logic (both Java DSL and YAML definitions).
-
Write end-to-end integration tests for REST resources that trigger workflows.
-
Verify automatic HTTP error mapping (RFC 7807 Problem Details).
-
Enable tracing and structured logging to debug test executions.
Prerequisites
-
A Quarkus application with Quarkus Flow set up.
-
JUnit 5 tests using
@QuarkusTest(orQuarkusUnitTestif writing extension tests). -
REST Assured (included in
quarkus-junit5-mockitoorquarkus-rest-client) for HTTP endpoint testing.
1. Unit-test workflow logic
You can test workflow execution logic directly without going through the HTTP layer. You simply inject the compiled Flow bean into your @QuarkusTest and assert the output state.
1.1 Test a Java DSL workflow
Assume you have a Java DSL workflow called HelloWorkflow. You can inject it by its class name or identifier:
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Inject;
import org.acme.HelloWorkflow;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.serverlessworkflow.impl.WorkflowModel;
@QuarkusTest (1)
class HelloWorkflowTest {
@Inject
HelloWorkflow workflow;
@Test
void should_produce_hello_message() throws Exception {
WorkflowModel result = workflow.instance(Map.of())
.start() (2)
.toCompletableFuture()
.get(5, TimeUnit.SECONDS);
// assuming the workflow writes {"message":"hello world!"}
assertThat(result.asMap().orElseThrow().get("message"), is("hello world!"));
}
}
In unit tests, it is perfectly acceptable to block execution using .await().indefinitely() (for Mutiny Uni) or .join() (for CompletionStage) to easily assert the final result.
|
1.2 Test a YAML-loaded workflow
Testing YAML workflows works exactly the same way. The engine compiles the YAML into a Flow bean at build time, meaning you test the actual runtime semantics, not just text parsing.
For a YAML workflow like echo-name.yaml (from Workflow definitions from YAML files):
import static org.hamcrest.Matchers.is;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import jakarta.inject.Inject;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.serverlessworkflow.impl.WorkflowDefinition;
import io.serverlessworkflow.impl.WorkflowModel;
import io.smallrye.common.annotation.Identifier;
@QuarkusTest
class EchoYamlWorkflowTest {
@Inject
@Identifier("flow:echo-name") // namespace:name from document section
WorkflowDefinition definition;
@Test
void should_echo_name_from_yaml_workflow() throws Exception {
WorkflowModel result = definition.instance(Map.of("name", "Joe"))
.start()
.toCompletableFuture()
.get(5, TimeUnit.SECONDS);
MatcherAssert.assertThat(result.asMap().orElseThrow().get("message"), is("echo: Joe"));
}
}
2. Test REST resources that invoke workflows
Most real-world applications expose workflows via HTTP endpoints. You should write integration tests using REST Assured to verify both the REST layer and the workflow execution.
Assume you have a modern, reactive JAX-RS resource like this:
import io.quarkiverse.flow.Flow;
import io.serverlessworkflow.api.WorkflowInstance;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Map;
@Path("/echo")
public class EchoResource {
@Inject
@Identifier("org.acme.EchoWorkflow")
Flow echoFlow;
@GET
public Uni<Map<String, Object>> echo(@QueryParam("name") String name) {
String finalName = (name != null && !name.isBlank()) ? name : "Duke";
return echoFlow.startInstance(Map.of("name", finalName))
.map(WorkflowInstance::output); // Return final workflow state
}
}
You can test this end-to-end using REST Assured:
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@QuarkusTest
class EchoResourceTest {
@Test
void should_echo_name_over_http() {
given()
.queryParam("name", "John")
.when()
.get("/echo")
.then()
.statusCode(200)
.body("message", equalTo("Hello, John!"));
}
}
3. Verify HTTP error mapping
If your workflow includes an HTTP or OpenAPI task that fails (e.g., calling a secured external API without credentials), Quarkus Flow automatically translates the internal WorkflowException into an RFC 7807 Problem Details HTTP response.
You should write tests to verify this mapping behaves correctly for your clients:
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@QuarkusTest
class CustomerProfileResourceTest {
@Test
void should_map_workflow_exception_to_problem_details() {
given()
.queryParam("customerId", "unauthorized-user")
.when()
.get("/customer/profile")
.then()
.statusCode(401)
.body("type", equalTo("https://serverlessworkflow.io/spec/1.0.0/errors/communication"))
.body("title", equalTo("HTTP 401 Unauthorized"))
.body("status", equalTo(401));
}
}
For this automatic error mapping to work, your JAX-RS resource must be reactive (returning a Uni or CompletionStage). If you block the thread with .await().indefinitely(), Java wraps the error in an ExecutionException, and the standard Quarkus mapper will not catch it.
|
4. Enable tracing for debugging tests
When a complex workflow test fails, it can be hard to know exactly which task broke. You can enable Quarkus Flow’s execution tracer specifically for your test runs to see exactly how the engine routed the data.
Add this to your application.properties:
# Enable tracing only during test execution
%test.quarkus.flow.tracing.enabled=true
When you run mvn test, the console will output a clear, linear history of every task start, completion, and failure for that specific workflow execution.
(For advanced structured logging with JSON and MDC fields, see Enable distributed tracing).
5. Tips for reliable workflow tests
-
Mock External Dependencies: Avoid calling real external APIs in unit tests. Use tools like
WireMockorTestcontainersto stub the endpoints your HTTP tasks call. -
Mock AI Agents: If you are testing LangChain4j workflows, mock the AI provider to avoid flaky tests caused by network latency or non-deterministic LLM generations.
-
Keep workflows focused: It is much easier to write exhaustive tests for a workflow that has a single responsibility than it is to test a massive, sprawling business process. Break complex logic into smaller subflows.
See also
-
Call HTTP and OpenAPI services — learn how HTTP errors are mapped and handled.
-
Define workflows from YAML — review the syntax for discovering and injecting YAML workflows.
-
Enable distributed tracing — read the full guide on MDC logs and trace extraction.