quarkus-tdd

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…

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill quarkus-tdd
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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() {

      // ARRANGE

      doNothing().when(orderRepository).persist(any(Order.class));

      // ACT

      OrderReceipt receipt = orderService.createOrder(validCommand);

      // ASSERT

      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() {

      // ARRANGE

      CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());

      // ACT & ASSERT

      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() {

      // ARRANGE

      doThrow(new PersistenceException("database unavailable"))

          .when(orderRepository).persist(any(Order.class));

      // ACT & ASSERT

      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() {

      // ACT & ASSERT

      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() {

    // ARRANGE - Test data

    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 {

      // ARRANGE

      MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);

      mockRabbitMQ.expectedMessageCount(1);

      // Replace real endpoint with mock for testing

      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("business-rules-publisher");

      // ACT

      producerTemplate.sendBody("direct:business-rules-publisher", testPayload);

      // ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson)

      mockRabbitMQ.assertIsSatisfied(5000);

      assertThat(mockRabbitMQ.getExchanges()).hasSize(1);

      String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);

      assertThat(body).contains("\"documentId\":1");

    }

    @Test

    @DisplayName("Should handle marshalling to JSON")

    void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {

      // ARRANGE

      MockEndpoint mockMarshal = new MockEndpoint("mock:marshal");

      camelContext.addEndpoint("mock:marshal", mockMarshal);

      mockMarshal.expectedMessageCount(1);

      camelContext.getRouteController().stopRoute("business-rules-publisher");

      AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {

        advice.weaveAddLast().to("mock:marshal");

      });

      camelContext.getRouteController().startRoute("business-rules-publisher");

      // ACT

      producerTemplate.sendBody("direct:business-rules-publisher", testPayload);

      // ASSERT

      mockMarshal.assertIsSatisfied(5000);

      String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class);

      assertThat(body).contains("\"documentId\":1");

      assertThat(body).contains("\"flowProfile\":\"BASIC\"");

    }

  }

  @Nested

  @DisplayName("Tests for document-processing route")

  class DocumentProcessing {

    @Test

    @DisplayName("Should route invoice to correct processor")

    void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {

      // ARRANGE

      MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class);

      mockInvoice.expectedMessageCount(1);

      camelContext.getRouteController().stopRoute("document-processing");

      AdviceWith.adviceWith(camelContext, "document-processing", advice -> {

        advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice");

      });

      camelContext.getRouteController().startRoute("document-processing");

      // ACT

      producerTemplate.sendBodyAndHeader("direct:process-document",

          testPayload, "documentType", "INVOICE");

      // ASSERT

      mockInvoice.assertIsSatisfied(5000);

    }

    @Test

    @DisplayName("Should handle validation errors gracefully")

    void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {

      // ARRANGE

      MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class);

      mockError.expectedMessageCount(1);

      camelContext.getRouteController().stopRoute("document-processing");

      AdviceWith.adviceWith(camelContext, "document-processing", advice -> {

        advice.weaveByToString(".*direct:validation-error-handler.*")

            .replace().to("mock:error");

      });

      camelContext.getRouteController().startRoute("document-processing");

      // Mock validator bean to throw exception

      when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document"));

      // ACT

      producerTemplate.sendBody("direct:process-document", testPayload);

      // ASSERT

      mockError.assertIsSatisfied(5000);

      Exception exception = mockError.getExchanges().get(0).getException();

      assertThat(exception).isInstanceOf(ValidationException.class);

      assertThat(exception.getMessage()).contains("Invalid document");

    }

  }

}

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() {

    // ARRANGE

    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 {

      // ARRANGE

      when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");

      // ACT

      assertDoesNotThrow(() ->

          eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));

      // ASSERT

      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")

    void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {

      // ARRANGE

      Object nullPayload = null;

      // ACT & ASSERT

      NullPointerException exception = assertThrows(

          NullPointerException.class,

          () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")

      );

      assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");

      verify(eventRepository, never()).persist(any());

    }

  }

  @Nested

  @DisplayName("Tests for createErrorEvent")

  class CreateErrorEvent {

    @Test

    @DisplayName("Should create error event with error message")

    void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception {

      // ARRANGE

      String errorMessage = "Processing failed";

      when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");

      // ACT

      assertDoesNotThrow(() ->

          eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));

      // ASSERT

      verify(eventRepository).persist(argThat(event ->

          event.getType().equals("PROCESSING_ERROR") &&

          event.getStatus() == EventStatus.ERROR &&

          event.getErrorMessage().equals(errorMessage) &&

          event.getPayload().equals("{\"documentId\":1}")

      ));

    }

    @ParameterizedTest

    @DisplayName("Should reject invalid error messages")

    @ValueSource(strings = {"", " "})

    void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {

      // ACT & ASSERT

      IllegalArgumentException exception = assertThrows(

          IllegalArgumentException.class,

          () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)

      );

      assertThat(exception.getMessage()).contains("Error message cannot be blank");

    }

  }

}

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() {

    // ARRANGE

    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 {

      // ARRANGE

      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());

      // ACT

      CompletableFuture<StoredDocumentInfo> future =

          fileStorageService.uploadOriginalFile(testInputStream, 1024L,

              testLogContext, InvoiceFormat.UBL);

      StoredDocumentInfo result = future.join();

      // ASSERT

      assertThat(result).isNotNull();

      assertThat(result.getPath()).isNotBlank();

      assertThat(result.getSize()).isEqualTo(1024L);

      assertThat(result.getUploadedAt()).isNotNull();

      verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));

    }

    @Test

    @DisplayName("Should handle S3 upload failure")

    void givenS3Failure_whenUpload_thenCompletableFutureFails() {

      // ARRANGE — run synchronously so exception propagates through the future

      doAnswer(invocation -> {

        ((Runnable) invocation.getArgument(0)).run();

        return null;

      }).when(executorService).execute(any(Runnable.class));

      when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))

          .thenThrow(new StorageException("S3 unavailable"));

      // ACT

      CompletableFuture<StoredDocumentInfo> future =

          fileStorageService.uploadOriginalFile(testInputStream, 1024L,

              testLogContext, InvoiceFormat.UBL);

      // ASSERT

      assertThatThrownBy(() -> future.join())

          .isInstanceOf(CompletionException.class)

          .hasCauseInstanceOf(StorageException.class)

          .hasMessageContaining("S3 unavailable");

    }

    @Test

    @DisplayName("Should propagate LogContext to async operation")

    void givenLogContext_whenUpload_thenContextPropagated() throws Exception {

      // ARRANGE

      AtomicReference<LogContext> capturedContext = new AtomicReference<>();

      doAnswer(invocation -> {

        capturedContext.set(CustomLog.getCurrentContext());

        ((Runnable) invocation.getArgument(0)).run();

        return null;

      }).when(executorService).execute(any(Runnable.class));

      // ACT

      fileStorageService.uploadOriginalFile(testInputStream, 1024L,

          testLogContext, InvoiceFormat.UBL).join();

      // ASSERT

      assertThat(capturedContext.get()).isNotNull();

      assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");

    }

  }

}

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() {

      // ARRANGE

      List<Document> documents = List.of(createDocument(1L, "DOC-001"));

      when(documentService.list(0, 20)).thenReturn(documents);

      // ACT &#x26; ASSERT

      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() {

      // ARRANGE

      Document document = createDocument(1L, "DOC-001");

      when(documentService.create(any())).thenReturn(document);

      // ACT &#x26; ASSERT

      given()

          .contentType(ContentType.JSON)

          .body("""

              {

                "referenceNumber": "DOC-001",

                "description": "Test document",

                "validUntil": "2030-01-01T00:00:00Z",

                "categories": ["test"]

              }

              """)

          .when().post("/api/documents")

          .then()

          .statusCode(201)

          .header("Location", containsString("/api/documents/1"))

          .body("referenceNumber", equalTo("DOC-001"));

    }

    @Test

    @DisplayName("Should return 400 for invalid input")

    void givenInvalidRequest_whenCreate_thenReturns400() {

      // ACT &#x26; ASSERT

      given()

          .contentType(ContentType.JSON)

          .body("""

              {

                "referenceNumber": "",

                "description": "Test"

              }

              """)

          .when().post("/api/documents")

          .then()

          .statusCode(400);

    }

  }

  private Document createDocument(Long id, String referenceNumber) {

    Document document = new Document();

    document.setId(id);

    document.setReferenceNumber(referenceNumber);

    document.setStatus(DocumentStatus.PENDING);

    return 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() {

    // ACT - Create via API

    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");

    // ASSERT - Retrieve via API

    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>

    <!-- Prepare agent for test execution -->

    <execution>

      <id>prepare-agent</id>

      <goals>

        <goal>prepare-agent</goal>

      </goals>

    </execution>

    <!-- Generate coverage report -->

    <execution>

      <id>report</id>

      <phase>verify</phase>

      <goals>

        <goal>report</goal>

      </goals>

    </execution>

    <!-- Enforce coverage thresholds -->

    <execution>

      <id>check</id>

      <goals>

        <goal>check</goal>

      </goals>

      <configuration>

        <rules>

          <rule>

            <element>BUNDLE</element>

            <limits>

              <limit>

                <counter>LINE</counter>

                <value>COVEREDRATIO</value>

                <minimum>0.80</minimum>

              </limit>

              <limit>

                <counter>BRANCH</counter>

                <value>COVEREDRATIO</value>

                <minimum>0.70</minimum>

              </limit>

            </limits>

          </rule>

        </rules>

      </configuration>

    </execution>

  </executions>

</plugin>

Run tests with coverage:

mvn clean test

mvn jacoco:report

mvn jacoco:check

# Report at: target/site/jacoco/index.html

Test Dependencies

<dependencies>

    <!-- Quarkus Testing -->

    <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>

    <!-- Mockito -->

    <dependency>

        <groupId>org.mockito</groupId>

        <artifactId>mockito-core</artifactId>

        <scope>test</scope>

    </dependency>

    <!-- AssertJ (preferred over JUnit assertions) -->

    <dependency>

        <groupId>org.assertj</groupId>

        <artifactId>assertj-core</artifactId>

        <version>3.24.2</version>

        <scope>test</scope>

    </dependency>

    <!-- REST Assured -->

    <dependency>

        <groupId>io.rest-assured</groupId>

        <artifactId>rest-assured</artifactId>

        <scope>test</scope>

    </dependency>

    <!-- Camel Testing -->

    <dependency>

        <groupId>org.apache.camel.quarkus</groupId>

        <artifactId>camel-quarkus-junit5</artifactId>

        <scope>test</scope>

    </dependency>

</dependencies>

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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card