Testing (Beta)

Summary: Guide to writing IM integration tests -- Spock framework, WireMock for HTTP mocking, test base classes, and common testing patterns.


Test Framework Overview

The Integration Manager uses Spock Framework with Groovy as its testing foundation. Tests are written as Spock specifications (classes extending Specification) and use the given/when/then block structure.

Key dependencies:

Dependency

Purpose

Spock Framework

BDD-style test framework for Groovy/Java

WireMock

HTTP mock server for simulating Pricefx API responses

Apache Camel Test

Camel context lifecycle and route testing utilities

Spring Test

Spring context bootstrapping via @ContextConfiguration

Testcontainers

Container-based infrastructure (e.g., SFTP servers)

All test files are Groovy (.groovy) and live under src/test/groovy/ in their respective modules.


Base Classes

The project provides a hierarchy of base specification classes. Choose the one that matches your test scope.

Class Hierarchy

Specification (Spock)
  └── WireMockSpecification              (integration-test module)
        └── UnitTestSpecification         (integration-test module, Spring-based)
              └── IntegrationTestSpecification  (integration-test module, repo-backed)
                    └── SftpTestSpecification    (integration-test module, SFTP container)

There is also a separate WireMockSpecification inside pricefx-integration for component-level tests that use the @CamelTest annotation instead of Spring.

When to Use Each

Base Class

Module

Use Case

WireMockSpecification (test.wiremock)

integration-test

Tests that only need an HTTP mock server and no Camel/Spring context

UnitTestSpecification

integration-test

Tests needing a Spring-managed CamelContext, property seeding, and connection/route/mapper/bean deployment

IntegrationTestSpecification

integration-test

Tests that load configuration from a FileSystemConfigurationRepository (i.e., a local repo/ directory)

SftpTestSpecification

integration-test

Tests that additionally require an SFTP server (via Testcontainers)

WireMockSpecification (pricefx-integration)

pricefx-integration

Component-level tests using @CamelTest with a standalone DefaultCamelContext and manual registry binding


Test Setup Approaches

There are two distinct approaches depending on which module you are writing tests in.

Approach 1: Spring-Based Tests (integration-test module)

Tests that extend UnitTestSpecification or IntegrationTestSpecification use Spring to manage the Camel context.

Groovy
package net.pricefx.integration.test.definition

import net.pricefx.integration.test.UnitTestSpecification
import org.apache.camel.CamelContext
import org.apache.camel.Exchange
import org.apache.camel.builder.AdviceWith
import org.springframework.beans.factory.annotation.Autowired

class MapperTest extends UnitTestSpecification {

    @Autowired
    private CamelContext context

    def "should test mapper"() {

        given:
        seedMapper('''<mappers>
    <loadMapper id="test">
        <body in="age" out="convertedAge" converter="stringToInteger"/>
        <body in="test" out="attribute1"/>
    </loadMapper>
</mappers>''')

        seedRoute('''
        <routes xmlns="http://camel.apache.org/schema/spring">
            <route id="test-mapper">
                <from uri="timer://foo?repeatCount=1"/>
                <to uri="pfx-model:transform?mapper=test"/>
            </route>
        </routes>
        ''')

        AdviceWith.adviceWith(context, "test-mapper", a ->
                a.replaceFromWith("direct:start")
        )

        when:
        Exchange exchange = sendBody("direct:start", [["test": "Jerry", "age": "42"]])

        then:
        exchange.getIn().getBody() == [["convertedAge": 42, "attribute1": "Jerry"]]
    }
}

What UnitTestSpecification provides:

  • @ContextConfiguration and @TestPropertySource annotations pre-configured

  • A Spring-managed CamelContext (autowired)

  • @TempDir for temporary file operations

  • Convenience methods: seedRoute(), seedMapper(), seedBean(), seedClass(), seedProperty(), seedDefaultConnection(), seedFile()

  • sendBody(endpointUri, body) to dispatch an exchange

  • expectedResponse(source) to load expected output from a file

  • Automatic context cleanup in setup() and cleanup()

What IntegrationTestSpecification adds:

  • A FileSystemConfigurationRepository loaded from src/test/resources/repo or src/main/resources/repo

  • seedEntities() to bulk-load beans, classes, mappers, and routes from the repository with optional include/exclude filtering by PRN

Approach 2: @CamelTest Annotation (pricefx-integration module)

Component-level tests in pricefx-integration use the custom @CamelTest Spock extension, which manages a standalone DefaultCamelContext outside of Spring.

Groovy
package net.pricefx.integration.component.api

import net.pricefx.integration.WireMockSpecification
import net.pricefx.integration.camel.CamelTest
// ...

@CamelTest
class PfxApiProducerFetchSpec extends WireMockSpecification {

    Exchange exchange
    PriceFxClient priceFxClient

    def setup() {
        cleanContext()
        def registry = new DefaultRegistry()
        registry.bind("filter", new Filter())

        priceFxClient = Mock(PriceFxClient.class)
        priceFxClient.generalApi >> Mock(GeneralDatasourceApiCompatible.class)
        // ... mock other APIs as needed

        registry.bind("pricefx", new PriceFxConnection(client: priceFxClient))
        exchange = new DefaultExchange(new DefaultCamelContext(registry))

        pfxApiEndpoint = new PfxApiEndpoint('pfx-api', 'fetch', new PfxApiComponent())
    }

    def "fetch products"() {
        given:
        pfxApiEndpoint.pfxApiConfiguration = new PfxApiConfiguration(
            objectType: ObjectType.P, filter: 'filter'
        )
        def pfxApiProducer = new PfxApiProducer(pfxApiEndpoint)

        when:
        pfxApiProducer.process(exchange)

        then:
        1 * priceFxClient.extendedProductApi.fetchproducts(_,_,_,_) >>
            new FetchResponse(response: new FetchResponseData(data: []))
        exchange.in.getHeader('totalRows') == 0
    }
}

What @CamelTest provides (via CamelTestExtension):

  • A fresh DefaultCamelContext with a DefaultRegistry, injected into the spec as context, registry, and producer

  • route(xml) -- deploy a route definition from an XML string

  • mapper(xml) -- deploy a mapper definition from an XML string

  • mock(uri) -- get a MockEndpoint by URI

  • sendBody(), sendBodyAndHeader(), sendBodyAndHeaders(), send(), asyncSend() -- producer template delegates

  • cleanContext() -- stop and remove all routes, reset mock endpoints

Note: @CamelTest should be applied at the class level. Each annotation spawns a new Camel context via introspection; applying it to multiple methods in the same class may produce unexpected state.


WireMock Patterns

WireMock is used to simulate the Pricefx API (and other HTTP endpoints) without requiring a live server.

How WireMock Is Initialized

Both WireMockSpecification classes use a shared singleton server on a dynamic port:

Groovy
@Shared
def wireMockServer = WireMockSingleton.getInstance()

WireMockSingleton creates a WireMockServer with WireMockConfiguration.options().dynamicPort() and starts it immediately.

Automatic JWT Stub

Both WireMock base classes automatically stub the Pricefx JWT authentication endpoint in setup():

Groovy
wireMockServer.stubFor(
    post(urlEqualTo('/pricefx/test-partition/accountmanager.getjsonwebtoken/'))
        .willReturn(okJson('{"response":{"node":"pa1","csrfToken":"...","data":["...JWT..."],"status":0}}'))
)

This means any test extending WireMockSpecification can make authenticated Pricefx API calls without additional auth setup.

Stubbing API Responses

Use standard WireMock static imports:

Groovy
import static com.github.tomakehurst.wiremock.client.WireMock.*

// Stub a POST endpoint returning JSON
wireMockServer.stubFor(
    post(urlEqualTo("/pricefx/mvich/datamart.getfcs/1234"))
        .withRequestBody(containing('"sortBy":["sku"]'))
        .willReturn(okJson('{"response":{"data":[],"status":0}}'))
)

Verifying Requests

The base class provides convenience methods:

Groovy
// Verify a POST was called exactly N times
verifyPost("/pricefx/mvich/some-endpoint", 2)

// Verify with payload matching
verifyPost("/pricefx/mvich/some-endpoint", 1, '{"expected":"json"}')

Or use WireMock's verify API directly:

Groovy
wireMockServer.verify(1, postRequestedFor(urlEqualTo("/some-url"))
    .withRequestBody(equalToJson('{"key":"value"}')))

Lifecycle

WireMock is reset in setup(), cleanup(), setupSpec(), and cleanupSpec() via wireMockServer.resetAll(), so stubs do not leak between tests.


Mocking Pricefx Clients Directly

For component-level tests (using @CamelTest), you can mock the PriceFxClient and its sub-APIs using Spock mocks instead of (or in addition to) WireMock:

Groovy
priceFxClient = Mock(PriceFxClient.class)
priceFxClient.generalApi >> Mock(GeneralDatasourceApiCompatible.class)
priceFxClient.lookuptableApi >> Mock(LookuptableApi.class)
priceFxClient.datamartApi >> Mock(DatamartApi.class)

registry.bind("pricefx", new PriceFxConnection(client: priceFxClient))

Then verify interactions in the then block:

Groovy
then:
1 * priceFxClient.getDatamartApi().datamartGetfcsFCTypeTypedIdOrSourceName("uniqueName", _ as FetchRequest) >> { arguments ->
    request = arguments[1]
    response
}

request.startRow == 2
request.endRow == 3

Example Test Structures

Pure Unit Test (no Camel context)

For logic that does not involve Camel routes:

Groovy
class IntegrationMapperMappingSpec extends Specification {

    private Exchange exchange
    private MapperProcessor mapperProcessor

    def setup() {
        exchange = new DefaultExchange(new DefaultCamelContext())
        exchange.getIn().setBody([["attribute1": "Attribute1Value"]])
        mapperProcessor = new MapperProcessor()
    }

    @Unroll
    def "should map properly"() {
        when:
        mapperProcessor.setMapper(createIntegrationMapper(definition))
        mapperProcessor.process(exchange)

        then:
        expected == exchange.getIn().getBody(List.class)[0][out]

        where:
        definition                           || expected          || out
        { im -> im.body("attribute1") }      || "Attribute1Value" || "attribute1"
    }
}

Data-Driven Test with where Block

Spock's where block and @Unroll are used extensively for parameterized tests:

Groovy
@Unroll
def "sortBy overrides filter"() {
    given:
    wireMockServer.stubFor(
        post(urlEqualTo("/pricefx/mvich/datamart.getfcs/1234"))
            .withRequestBody(containing(sortByInCall))
            .willReturn(okJson('{"response":{"data":[],"status":0}}'))
    )

    and:
    route('''
    <routes xmlns="http://camel.apache.org/schema/spring">
        <route>
            <from uri="direct:start"/>
            <toD uri="pfx-api:fetchFcs?objectType=DM&amp;typedId=1234''' + uriSuffix + '''"/>
            <to uri="mock:end"/>
        </route>
    </routes>
    ''')

    when:
    sendBody("direct:start", "")

    then:
    def endpoint = mock("mock:end")
    endpoint.receivedExchanges.size() == 1

    where:
    uriSuffix                | sortByInCall
    ''                       | ''
    '&amp;sortBy=sku'        | '"sortBy":["sku"],'
    '&amp;sortBy=sku,label'  | '"sortBy":["sku","label"],'
}

Route Test with AdviceWith

When testing routes that have a timer or other non-direct consumer, use AdviceWith to replace the from endpoint:

Groovy
AdviceWith.adviceWith(context, "route-id", a ->
    a.replaceFromWith("direct:start")
)

Exchange exchange = sendBody("direct:start", inputBody)

Bean Test

Groovy
class BeansTest extends UnitTestSpecification {

    @Autowired
    private CamelContext context

    def "should test bean"() {
        given:
        seedBean('''<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
    <util:list id="myList" value-type="java.lang.String">
        <value>foo</value>
        <value>bar</value>
    </util:list>
</beans>''')

        seedRoute('''
        <routes xmlns="http://camel.apache.org/schema/spring">
            <route id="test-bean">
                <from uri="timer://foo?repeatCount=1"/>
                <setBody>
                    <method ref="myList" method="toString"/>
                </setBody>
            </route>
        </routes>
        ''')

        AdviceWith.adviceWith(context, "test-bean", a ->
                a.replaceFromWith("direct:start")
        )

        when:
        Exchange exchange = sendBody("direct:start", [])

        then:
        exchange.getIn().getBody() == "[foo, bar]"
    }
}

Groovy Class (Processor) Test

Testing a Groovy processor class that transforms data in a route:

Groovy
class GroovyProcessorTest extends UnitTestSpecification {

    @Autowired
    private CamelContext context

    def "should transform body using groovy processor"() {

        given:
        seedClass('''package repo.classes

import org.apache.camel.Exchange
import org.apache.camel.Processor

class SkuPrefixProcessor implements Processor {

    @Override
    void process(Exchange exchange) throws Exception {
        def rows = exchange.getIn().getBody(List)
        rows.each { row ->
            row.sku = "PFX-" + row.sku
        }
        exchange.getIn().setBody(rows)
    }
}''')

        seedRoute('''
        <routes xmlns="http://camel.apache.org/schema/spring">
            <route id="test-groovy-processor">
                <from uri="timer://foo?repeatCount=1"/>
                <to uri="bean:skuPrefixProcessor"/>
            </route>
        </routes>
        ''')

        AdviceWith.adviceWith(context, "test-groovy-processor", a ->
                a.replaceFromWith("direct:start")
        )

        when:
        Exchange exchange = sendBody("direct:start", [
            [sku: "001", label: "Widget A"],
            [sku: "002", label: "Widget B"]
        ])

        then:
        def result = exchange.getIn().getBody(List)
        result[0].sku == "PFX-001"
        result[1].sku == "PFX-002"
        result[0].label == "Widget A"

    }

}

File Import Test (flat directory)

Testing a file-triggered route that loads CSV into Pricefx, with the file directly in the watched directory:

Groovy
class FileImportTest extends IntegrationTestSpecification {

    def "should load data from csv to pricefx partition"() {

        given: 'mock pricefx endpoint and seed entities'
        mockPost('/pricefx/test-partition/loaddata/P')
        seedProperty("data.directory", temporaryFolder.toString())
        seedEntities([routePrn("load-products-route.xml"), mapperPrn("product-mapper.xml")])

        when: 'csv file is created in data directory'
        seedFile("data/products.csv")

        then: 'verify that route converts csv to json and sends it to pricefx'
        def conditions = new PollingConditions(timeout: 10)
        conditions.eventually {
            verifyPost("/pricefx/test-partition/loaddata/P", 1,
                    expectedResponse("requests/load-products-request.json"))
        }

    }

}

File Import Test (subdirectory)

When the route watches a subdirectory (file:{{data.directory}}/import/products), use seedFile with the target subdirectory parameter:

Groovy
class FileImportSubdirTest extends IntegrationTestSpecification {

    def "should load data from csv in subdirectory"() {

        given: 'mock pricefx endpoint and seed entities'
        mockPost('/pricefx/test-partition/loaddata/P')
        seedProperty("data.directory", temporaryFolder.toString())
        seedEntities([routePrn("load-products-subdir-route.xml"), mapperPrn("product-mapper.xml")])

        when: 'csv file is placed in the import/products subdirectory'
        seedFile("data/products.csv", "import/products")

        then: 'verify that route picks up file from subdirectory'
        def conditions = new PollingConditions(timeout: 10)
        conditions.eventually {
            verifyPost("/pricefx/test-partition/loaddata/P", 1,
                    expectedResponse("requests/load-products-request.json"))
        }

    }

}

seedFile("data/products.csv", "import/products") reads products.csv from the classpath and places it in temporaryFolder/import/products/products.csv, creating directories automatically.

Quartz/Timer Route Test

For routes triggered by a cron schedule, use seedTriggeredRoute to replace the quartz: consumer with direct:, then triggerRoute to fire it manually:

Groovy
class QuartzExportTest extends IntegrationTestSpecification {

    def "should export products on quartz trigger"() {

        given: 'mock pricefx fetch endpoint and seed quartz route'
        mockPost('/pricefx/test-partition/productmanager.fetchproducts',
                '{"response":{"data":[{"sku":"P-001","label":"BMW"}],"status":0}}')
        seedDefaultConnection()
        seedTriggeredRoute("export-products-quartz.xml")

        when: 'route is triggered manually'
        triggerRoute("export-products-quartz")

        then: 'products were fetched from pricefx'
        verifyPost("/pricefx/test-partition/productmanager.fetchproducts", 1)

    }

}

seedTriggeredRoute works with quartz:, timer:, pfx-event:, kafka:, aws2-s3:, and rest: consumers — it replaces any of them with a direct: endpoint.

Quartz Lifecycle Test (stop and restart)

To verify a scheduled route can be paused and resumed via the Camel RouteController, use the same seedTriggeredRoute setup, then exercise the lifecycle API directly:

Groovy
class QuartzLifecycleTest extends IntegrationTestSpecification {

    def "scheduled route can be stopped and restarted"() {

        given: 'seed quartz route and mock the downstream API'
        mockPost('/pricefx/test-partition/productmanager.fetchproducts',
                '{"response":{"data":[],"status":0}}')
        seedDefaultConnection()
        seedTriggeredRoute("export-products-quartz.xml")

        def routeId = "export-products-quartz"
        def controller = context.getRouteController()

        expect: 'route starts in Started state'
        controller.getRouteStatus(routeId) == org.apache.camel.ServiceStatus.Started

        when: 'route is stopped'
        controller.stopRoute(routeId)

        then: 'status reflects stopped — sending to it now fails'
        controller.getRouteStatus(routeId) == org.apache.camel.ServiceStatus.Stopped
        sendBody("direct://trigger-${routeId}", "ignored").isFailed()

        when: 'route is restarted and triggered'
        controller.startRoute(routeId)
        triggerRoute(routeId)

        then: 'pfx-api received the new trigger'
        verifyPost("/pricefx/test-partition/productmanager.fetchproducts", 1)

    }

}

Useful when partners need to verify their integration's start/stop hooks or guard against double-processing while a route is paused.

REST Inbound Route Test

For routes that expose an HTTP endpoint via rest:get:/path or rest:post:/path, seedTriggeredRoute rewrites the consumer to direct:trigger-{routeId} so the REST DSL servlet is not required at test time. triggerRoute(routeId, body) then plays the role of an inbound HTTP request:

Groovy
class RestPingTest extends IntegrationTestSpecification {

    def "GET /v1/ping returns the health body"() {

        given: 'seed REST inbound route — rest: becomes direct:'
        seedTriggeredRoute("rest-inbound-ping.xml")

        when: 'route is invoked as if the REST DSL servlet received a GET'
        def exchange = triggerRoute("rest-inbound-ping")

        then: 'route produced the configured JSON body without failing'
        !exchange.isFailed()
        exchange.getMessage().getBody(String.class) == '{"status":"ok"}'

    }

    def "POST /v1/products returns 400 for invalid payload"() {

        given:
        seedTriggeredRoute("rest-inbound-products.xml")

        when:
        def exchange = triggerRoute("rest-inbound-products", [sku: "P-001"]) // price missing

        then: 'doCatch path set CamelHttpResponseCode=400 and a JSON error body'
        !exchange.isFailed()
        exchange.getMessage().getHeader("CamelHttpResponseCode").toString() == "400"
        exchange.getMessage().getBody(String.class).contains("Missing mandatory fields")

    }

}

The body passed to triggerRoute(routeId, body) simulates what the REST DSL would unmarshal from the request body (e.g. a Map when binding-mode=json). Set CamelHttpResponseCode in the route (not the literal string Exchange.HTTP_RESPONSE_CODE) — that is the actual constant value Camel uses when serializing the HTTP response.

Event-Driven Route Test

For routes that consume events via pfx-event:fetch, use EventTestSpecification:

Groovy
class EventHandlerTest extends EventTestSpecification {

    def "should route PADATALOAD event to correct handler"() {

        given: 'seed event route'
        seedEventRoute("event-fetch-route.xml")

        when: 'PADATALOAD event is received'
        simulateEvent("eventFetch", "PADATALOAD_COMPLETED")

        then: 'event was processed and routed to dataload handler'
        eventProcessedSuccessfully()
        context.getEndpoint("mock:dataload-handler").receivedExchanges.size() == 1

    }

    def "should pass extra fields to handler"() {

        given: 'seed event route'
        seedEventRoute("event-fetch-route.xml")

        when: 'event with extra fields is received'
        simulateEvent("eventFetch", "PADATALOAD_COMPLETED", [
                sourceTable: "Products",
                recordCount: "1500"
        ])

        then: 'handler receives the extra fields in body'
        eventProcessedSuccessfully()
        def body = context.getEndpoint("mock:dataload-handler")
                .receivedExchanges[0].getIn().getBody(Map)
        body.sourceTable == "Products"

    }

}

seedEventRoute replaces pfx-event:fetch with direct: before deploying. simulateEvent sends a Map body matching the real event format ([eventType: "...", ...]) with event.typedId and event.name headers.

Groovy Class in a File Import Route

A common partner pattern: a Groovy processor validates or transforms rows during import. Test both the Groovy class and the route together:

Groovy
class ImportWithGroovyValidationTest extends UnitTestSpecification {

    @Autowired
    private CamelContext context

    def "should skip rows where sku is empty using groovy filter"() {

        given: 'seed groovy filter class'
        seedClass('''package repo.classes

import org.apache.camel.Exchange
import org.apache.camel.Processor

class SkuValidationProcessor implements Processor {

    @Override
    void process(Exchange exchange) throws Exception {
        def rows = exchange.getIn().getBody(List)
        def validRows = rows.findAll { row -> row.sku != null && !row.sku.trim().isEmpty() }
        exchange.getIn().setBody(validRows)
    }
}''')

        and: 'seed mapper and route'
        seedMapper('''<mappers>
    <loadMapper id="validated-product-mapper">
        <body in="sku" out="sku"/>
        <body in="label" out="label"/>
    </loadMapper>
</mappers>''')

        mockPost('/pricefx/test-partition/loaddata/P')
        seedDefaultConnection()

        seedRoute('''
        <routes xmlns="http://camel.apache.org/schema/spring">
            <route id="import-with-validation">
                <from uri="direct:start"/>
                <to uri="pfx-csv:unmarshal?skipHeaderRecord=true"/>
                <to uri="bean:skuValidationProcessor"/>
                <to uri="pfx-api:loaddata?objectType=P&amp;mapper=validated-product-mapper"/>
            </route>
        </routes>
        ''')

        when: 'csv with empty sku rows is sent'
        Exchange exchange = sendBody("direct:start", "sku,label
PROD-001,Widget A
,Empty Row
PROD-002,Widget B")

        then: 'only valid rows are loaded (2 out of 3)'
        verifyPost("/pricefx/test-partition/loaddata/P", 1)

    }

}

Running Tests

Unit Tests (default)

Bash
# Run all unit tests across all modules
mvn test

# Run unit tests in a specific module
mvn test -pl pricefx-integration

# Run a single test class
mvn test -pl pricefx-integration -Dtest="PfxApiProducerFetchSpec"

Integration Tests

The project defines Maven profiles for integration test execution:

Bash
# Run integration tests (uses maven-failsafe-plugin)
mvn verify -P integration-tests

# Run Camel-specific integration tests
mvn verify -P integration-camel-tests

The integration-tests profile sets spock.test.category=IntegrationTests and the integration-camel-tests profile sets spock.test.category=IntegrationCamelTests. Both profiles disable the surefire plugin (unit tests) so only failsafe (integration tests) runs.

Java Version Requirement

Tests require Java 17. If your default mvn uses a different JDK, set JAVA_HOME before running:

Bash
export JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/17.0.14/libexec/openjdk.jdk/Contents/Home
mvn test

seedEntities and Production Files

seedEntities() loads configuration directly from the project's FileSystemConfigurationRepository — the same files used in production. This is intentional: it tests real mappers and routes, not copies.

Groovy
class MyIntegrationTest extends IntegrationTestSpecification {

    def setup() {
        // Load all beans, routes, mappers from src/main/resources/repo/
        seedEntities()

        // Or load selectively by PRN prefix
        seedEntities(include: ["routes/import-products"])
    }
}

Limitations of seedFile():

  • seedFile(path, content) only supports flat paths — no subdirectory separators. Files must be placed directly in the category root (e.g., mappers/my-mapper.xml, not mappers/sub/my-mapper.xml).

  • For subdirectory structures, use IntegrationTestSpecification with seedEntities() instead.

Testing Async Routes

For routes triggered by a scheduler or file consumer, use Spock's PollingConditions to wait for async processing:

Groovy
import spock.util.concurrent.PollingConditions

def conditions = new PollingConditions(timeout: 10, delay: 0.5)

when:
// trigger or drop a file that starts the route

then:
conditions.eventually {
    // assert the expected side effect (e.g., WireMock received a request)
    wireMockServer.verify(1, postRequestedFor(urlPathMatching("/pricefx/.*/loaddata/.*")))
}

PollingConditions.eventually {} retries the block until it passes or the timeout expires. Prefer this over Thread.sleep().


Key Conventions

  1. Test file naming: Spec classes end with Spec (for Surefire) or Test (for both Surefire and Failsafe). The naming determines which plugin picks them up.

  2. WireMock partition: The default stubbed partition is test-partition in the integration-test module and mvich in the pricefx-integration module.

  3. Clean state: Both base classes reset the WireMock server and Camel context between tests. Avoid relying on state from a previous test method.

  4. Repository-backed tests: Place test configuration files (routes, mappers, beans, classes, properties) under src/test/resources/repo/ when using IntegrationTestSpecification. For integration tests that use real production config, use seedEntities() directly.

  5. Use @Unroll for parameterized tests: Produces a separate test report entry per where row, making failures easier to diagnose.

  6. Prefer direct:start endpoints: Replace timer or other consumers with direct:start using AdviceWith or by defining routes with direct:start directly.

  7. Use PollingConditions for async routes: Never sleep — use PollingConditions.eventually {} to wait for side effects in routes triggered by timers or file consumers.