| name | quarkus-tdd |
| description | Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services. |
| cluster | jvm |
| version | 1.0.0 |
Quarkus TDD Workflow
TDD guidance for Quarkus 3.x services with 80%+ coverage (unit + integration). Optimized for event-driven architectures with Apache Camel.
When to Use
- New features or REST endpoints
- Bug fixes or refactors
- Adding data access logic, security rules, or reactive streams
- Testing Apache Camel routes and event handlers
- Testing event-driven services with RabbitMQ
- Testing conditional flow logic
- Validating CompletableFuture async operations
- Testing LogContext propagation
Workflow
- Write tests first (they should fail)
- Implement minimal code to pass
- Refactor with tests green
- Enforce coverage with JaCoCo (80%+ target)
Unit Tests with @Nested Organization
Follow this structured approach for comprehensive, readable tests:
@ExtendWith(MockitoExtension.class)
@DisplayName("OrderService Unit Tests")
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private EventService eventService;
@Mock
private FulfillmentPublisher fulfillmentPublisher;
@InjectMocks
private OrderService orderService;
private CreateOrderCommand validCommand;
@BeforeEach
void setUp() {
validCommand = new CreateOrderCommand(
"customer-123",
List.of(new OrderLine("sku-123", 2))
);
}
@Nested
@DisplayName("Tests for createOrder")
class CreateOrder {
@Test
@DisplayName("Should persist order and publish fulfillment event")
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
doNothing().when(orderRepository).persist(any(Order.class));
OrderReceipt receipt = orderService.createOrder(validCommand);
assertThat(receipt).isNotNull();
assertThat(receipt.customerId()).isEqualTo("customer-123");
verify(orderRepository).persist(any(Order.class));
verify(fulfillmentPublisher).publishAsync(receipt);
verify(eventService).createSuccessEvent(receipt, "ORDER_CREATED");
}
@Test
@DisplayName("Should reject missing customer id")
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
WebApplicationException exception = assertThrows(
WebApplicationException.class,
() -> orderService.createOrder(invalid)
);
assertThat(exception.getResponse().getStatus()).isEqualTo(400);
verify(orderRepository, never()).persist(any(Order.class));
verify(fulfillmentPublisher, never()).publishAsync(any());
}
@Test
@DisplayName("Should record error event when persistence fails")
void givenPersistenceFailure_whenCreateOrder_thenRecordsErrorEvent() {
doThrow(new PersistenceException("database unavailable"))
.when(orderRepository).persist(any(Order.class));
PersistenceException exception = assertThrows(
PersistenceException.class,
() -> orderService.createOrder(validCommand)
);
assertThat(exception.getMessage()).contains("database unavailable");
verify(eventService).createErrorEvent(
eq(validCommand),
eq("ORDER_CREATE_FAILED"),
contains("database unavailable")
);
verify(fulfillmentPublisher, never()).publishAsync(any());
}
@Test
@DisplayName("Should reject null commands")
void givenNullCommand_whenCreateOrder_thenThrowsNullPointerException() {
assertThrows(
NullPointerException.class,
() -> orderService.createOrder(null)
);
verify(orderRepository, never()).persist(any(Order.class));
}
}
}
Key Testing Patterns
- @Nested Classes: Group tests by method being tested
- @DisplayName: Provide readable test descriptions for test reports
- Naming Convention:
givenX_whenY_thenZ for clarity
- AAA Pattern: Explicit
// ARRANGE, // ACT, // ASSERT comments
- @BeforeEach: Setup common test data to reduce duplication
- assertDoesNotThrow: Test success scenarios without catching exceptions
- assertThrows: Test exception scenarios with message validation using AssertJ
- Comprehensive Coverage: Test happy paths, null inputs, edge cases, exceptions
- Verify Interactions: Use Mockito
verify() to ensure methods are called correctly
- Never Verify: Use
never() to ensure methods are NOT called in error scenarios
Testing Camel Routes
@QuarkusTest
@DisplayName("Business Rules Camel Route Tests")
class BusinessRulesRouteTest {
@Inject
CamelContext camelContext;
@Inject
ProducerTemplate producerTemplate;
@InjectMock
EventService eventService;
@InjectMock
DocumentValidator documentValidator;
private BusinessRulesPayload testPayload;
@BeforeEach
void setUp() {
testPayload = new BusinessRulesPayload();
testPayload.setDocumentId(1L);
testPayload.setFlowProfile(FlowProfile.BASIC);
}
@Nested
@DisplayName("Tests for business-rules-publisher route")
class BusinessRulesPublisher {
@Test
@DisplayName("Should successfully publish message to RabbitMQ")
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
mockRabbitMQ.expectedMessageCount(1);
camelContext.getRouteController().stopRoute("business-rules-publisher");
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
advice.replaceFromWith("direct:business-rules-publisher");
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
});
camelContext.getRouteController().startRoute();
producerTemplate.sendBody(, testPayload);
mockRabbitMQ.assertIsSatisfied();
assertThat(mockRabbitMQ.getExchanges()).hasSize();
mockRabbitMQ.getExchanges().get().getIn().getBody(String.class);
assertThat(body).contains();
}
Exception {
();
camelContext.addEndpoint(, mockMarshal);
mockMarshal.expectedMessageCount();
camelContext.getRouteController().stopRoute();
AdviceWith.adviceWith(camelContext, , advice -> {
advice.weaveAddLast().to();
});
camelContext.getRouteController().startRoute();
producerTemplate.sendBody(, testPayload);
mockMarshal.assertIsSatisfied();
mockMarshal.getExchanges().get().getIn().getBody(String.class);
assertThat(body).contains();
assertThat(body).contains();
}
}
{
Exception {
camelContext.getEndpoint(, MockEndpoint.class);
mockInvoice.expectedMessageCount();
camelContext.getRouteController().stopRoute();
AdviceWith.adviceWith(camelContext, , advice -> {
advice.weaveByToString().replace().to();
});
camelContext.getRouteController().startRoute();
producerTemplate.sendBodyAndHeader(,
testPayload, , );
mockInvoice.assertIsSatisfied();
}
Exception {
camelContext.getEndpoint(, MockEndpoint.class);
mockError.expectedMessageCount();
camelContext.getRouteController().stopRoute();
AdviceWith.adviceWith(camelContext, , advice -> {
advice.weaveByToString()
.replace().to();
});
camelContext.getRouteController().startRoute();
(documentValidator.validate(any())).thenThrow( ());
producerTemplate.sendBody(, testPayload);
mockError.assertIsSatisfied();
mockError.getExchanges().get().getException();
assertThat(exception).isInstanceOf(ValidationException.class);
assertThat(exception.getMessage()).contains();
}
}
}
Testing Event Services
@ExtendWith(MockitoExtension.class)
@DisplayName("EventService Unit Tests")
class EventServiceTest {
@Mock
private EventRepository eventRepository;
@Mock
private ObjectMapper objectMapper;
@InjectMocks
private EventService eventService;
private BusinessRulesPayload testPayload;
@BeforeEach
void setUp() {
testPayload = new BusinessRulesPayload();
testPayload.setDocumentId(1L);
}
@Nested
@DisplayName("Tests for createSuccessEvent")
class CreateSuccessEvent {
@Test
@DisplayName("Should create success event with correct attributes")
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
assertDoesNotThrow(() ->
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
verify(eventRepository).persist(argThat(event ->
event.getType().equals("DOCUMENT_PROCESSED") &&
event.getStatus() == EventStatus.SUCCESS &&
event.getPayload().equals("{\"documentId\":1}") &&
event.getTimestamp() != null
));
}
@Test
@DisplayName("Should throw exception when payload is null")
{
;
assertThrows(
NullPointerException.class,
() -> eventService.createSuccessEvent(nullPayload, )
);
assertThat(exception.getMessage()).isEqualTo();
verify(eventRepository, never()).persist(any());
}
}
{
Exception {
;
(objectMapper.writeValueAsString(testPayload)).thenReturn();
assertDoesNotThrow(() ->
eventService.createErrorEvent(testPayload, , errorMessage));
verify(eventRepository).persist(argThat(event ->
event.getType().equals() &&
event.getStatus() == EventStatus.ERROR &&
event.getErrorMessage().equals(errorMessage) &&
event.getPayload().equals()
));
}
{
assertThrows(
IllegalArgumentException.class,
() -> eventService.createErrorEvent(testPayload, , blankMessage)
);
assertThat(exception.getMessage()).contains();
}
}
}
Testing CompletableFuture
@ExtendWith(MockitoExtension.class)
@DisplayName("FileStorageService Unit Tests")
class FileStorageServiceTest {
@Mock
private S3Client s3Client;
@Mock
private ExecutorService executorService;
@InjectMocks
private FileStorageService fileStorageService;
private InputStream testInputStream;
private LogContext testLogContext;
@BeforeEach
void setUp() {
testInputStream = new ByteArrayInputStream("test content".getBytes());
testLogContext = new LogContext();
testLogContext.put("traceId", "trace-123");
}
@Nested
@DisplayName("Tests for uploadOriginalFile")
class UploadOriginalFile {
@Test
@DisplayName("Should successfully upload file and return document info")
void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception {
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
}).when(executorService).execute(any(Runnable.class));
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
.thenReturn(PutObjectResponse.builder().build());
CompletableFuture<StoredDocumentInfo> future =
fileStorageService.uploadOriginalFile(testInputStream, ,
testLogContext, InvoiceFormat.UBL);
future.join();
assertThat(result).isNotNull();
assertThat(result.getPath()).isNotBlank();
assertThat(result.getSize()).isEqualTo();
assertThat(result.getUploadedAt()).isNotNull();
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
}
{
doAnswer(invocation -> {
((Runnable) invocation.getArgument()).run();
;
}).(executorService).execute(any(Runnable.class));
(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
.thenThrow( ());
CompletableFuture<StoredDocumentInfo> future =
fileStorageService.uploadOriginalFile(testInputStream, ,
testLogContext, InvoiceFormat.UBL);
assertThatThrownBy(() -> future.join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(StorageException.class)
.hasMessageContaining();
}
Exception {
AtomicReference<LogContext> capturedContext = <>();
doAnswer(invocation -> {
capturedContext.set(CustomLog.getCurrentContext());
((Runnable) invocation.getArgument()).run();
;
}).(executorService).execute(any(Runnable.class));
fileStorageService.uploadOriginalFile(testInputStream, ,
testLogContext, InvoiceFormat.UBL).join();
assertThat(capturedContext.get()).isNotNull();
assertThat(capturedContext.get().get()).isEqualTo();
}
}
}
Resource Layer Tests (REST Assured)
@QuarkusTest
@DisplayName("DocumentResource API Tests")
class DocumentResourceTest {
@InjectMock
DocumentService documentService;
@Nested
@DisplayName("Tests for GET /api/documents")
class ListDocuments {
@Test
@DisplayName("Should return list of documents")
void givenDocumentsExist_whenList_thenReturnsOk() {
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
when(documentService.list(0, 20)).thenReturn(documents);
given()
.when().get("/api/documents")
.then()
.statusCode(200)
.body("$.size()", is(1))
.body("[0].referenceNumber", equalTo("DOC-001"));
}
}
@Nested
@DisplayName("Tests for POST /api/documents")
class CreateDocument {
@Test
@DisplayName("Should create document and return 201")
void givenValidRequest_whenCreate_thenReturns201() {
Document document = createDocument(1L, "DOC-001");
when(documentService.create(any())).thenReturn(document);
given()
.contentType(ContentType.JSON)
.body()
.().post()
.then()
.statusCode()
.header(, containsString())
.body(, equalTo());
}
{
given()
.contentType(ContentType.JSON)
.body()
.().post()
.then()
.statusCode();
}
}
Document {
();
document.setId(id);
document.setReferenceNumber(referenceNumber);
document.setStatus(DocumentStatus.PENDING);
document;
}
}
Integration Tests with Real Database
@QuarkusTest
@TestProfile(IntegrationTestProfile.class)
@DisplayName("Document Integration Tests")
class DocumentIntegrationTest {
@Test
@Transactional
@DisplayName("Should create and retrieve document via API")
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
Long id = given()
.contentType(ContentType.JSON)
.body("""
{
"referenceNumber": "INT-001",
"description": "Integration test",
"validUntil": "2030-01-01T00:00:00Z",
"categories": ["test"]
}
""")
.when().post("/api/documents")
.then()
.statusCode(201)
.extract().path("id");
given()
.when().get("/api/documents/" + id)
.then()
.statusCode(200)
.body("referenceNumber", equalTo("INT-001"));
}
}
Coverage with JaCoCo
Maven Configuration (Complete)
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</>
check
BUNDLE
LINE
COVEREDRATIO
0.80
BRANCH
COVEREDRATIO
0.70
Run tests with coverage:
mvn clean test
mvn jacoco:report
mvn jacoco:check
Test Dependencies
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</>
assertj-core
3.24.2
test
io.rest-assured
rest-assured
test
org.apache.camel.quarkus
camel-quarkus-junit5
test
Best Practices
Test Organization
- Use
@Nested classes to group tests by method being tested
- Use
@DisplayName for readable test descriptions visible in reports
- Follow
givenX_whenY_thenZ naming convention for test methods
- Use
@BeforeEach for common test data setup to reduce duplication
Test Structure
- Follow AAA pattern with explicit comments (
// ARRANGE, // ACT, // ASSERT)
- Use
assertDoesNotThrow for success scenarios
- Use
assertThrows for exception scenarios with message validation
- Verify exception messages match expected values using AssertJ
contains() or isEqualTo()
Test Coverage
- Test happy paths for all public methods
- Test null input handling
- Test edge cases (empty collections, boundary values, negative IDs, blank strings)
- Test exception scenarios comprehensively
- Mock all external dependencies (repositories, services, Camel endpoints)
- Aim for 80%+ line coverage, 70%+ branch coverage
Assertions
- Prefer AssertJ (
assertThat) over JUnit assertions for value checks
- Use fluent AssertJ API for readability:
assertThat(list).hasSize(3).contains(item)
- For exceptions: use JUnit
assertThrows to capture, then AssertJ to validate the message
- For non-throwing success paths: use JUnit
assertDoesNotThrow
- For collections:
extracting(), filteredOn(), containsExactly()
Testing Integration
- Use
@QuarkusTest for integration tests
- Use
@InjectMock to mock dependencies in Quarkus tests
- Prefer REST Assured for API testing
- Use
@TestProfile for test-specific configuration
Event-Driven Testing
- Test Camel routes with
AdviceWith and MockEndpoint
- Use
@CamelQuarkusTest annotation (if using standalone Camel tests)
- Verify message content, headers, and routing logic
- Test error handling routes separately
- Mock external systems (RabbitMQ, S3, databases) in unit tests
Camel Route Testing
- Use
MockEndpoint for asserting message flow
- Use
AdviceWith to modify routes for testing (replace endpoints with mocks)
- Test message transformation and marshalling
- Test exception handling and dead letter queues
Testing Async Operations
- Test CompletableFuture success and failure scenarios
- Use
.join() in tests to wait for async completion
- Test exception propagation from CompletableFuture
- Verify LogContext propagation to async operations
Performance
- Keep tests fast and isolated
- Run tests in continuous mode:
mvn quarkus:test
- Use parameterized tests (
@ParameterizedTest) for input variations
- Build reusable test data builders or factory methods
Quarkus-Specific
- Stay on latest LTS version (Quarkus 3.x)
- Test native compilation compatibility periodically
- Use Quarkus test profiles for different scenarios
- Leverage Quarkus dev services for local testing
- Use
@InjectMock instead of @MockBean (Quarkus-specific)
Verification Best Practices
- Always verify interactions on mocked dependencies
- Use
verify(mock, never()) to ensure methods are NOT called in error scenarios
- Use
argThat() for complex argument matching
- Verify the order of calls when it matters:
InOrder from Mockito