Testing
FTSnext uses a multi-layer testing strategy to ensure correctness at different levels of abstraction. This page explains the test types, how to run them, and the conventions to follow when writing new tests.
Test Types
| Type | Suffix | Location | Runs With |
|---|---|---|---|
| Unit | *Test.java | src/test/java/ | mvn test |
| Integration | *IT.java | src/test/java/ | mvn verify |
| Agent E2E | *E2E.java | src/e2e/java/ | mvn verify -Pe2e |
| E2E | Shell scripts | .github/test/ | make (Docker Compose) |
Running Tests
Unit Tests
# Run all unit tests
mvn clean test
# Run a specific test class
mvn test -Dtest=EverythingDataSelectorConfigTest
# Run a single test method
mvn test -Dtest=EverythingDataSelectorConfigTest#nullPageSizeUsesDefaultIntegration Tests
# Run all unit and integration tests
mvn clean verify
# Run tests for a specific agent
mvn clean verify --projects clinical-domain-agent --also-makeBuilding Docker Images
Both agent E2E and E2E tests require locally built Docker images. Build them from the project root with:
makeAgent E2E Tests
Agent E2E tests run against real Docker containers using Testcontainers.
# Run all agent E2E tests for a specific agent
mvn clean verify -Pe2e --projects clinical-domain-agent --also-make
# Run a specific E2E test
mvn clean verify -Pe2e -Dit.test=TCACohortSelectorE2E \
--projects clinical-domain-agent --also-makeE2E Test
The full end-to-end test starts all agents with their external dependencies (Blaze FHIR servers, gICS, gPAS, Keycloak, Redis) via Docker Compose and drives real transfer processes through them.
Layout
.github/test/
├── compose.yaml # Top-level Compose, includes per-agent + oauth2 stacks
├── Makefile # All `make` targets the scenarios call
├── cd-agent/ # CDA application.yaml + project YAMLs (transfer configs)
├── tc-agent/
├── rd-agent/
├── ssl/ # Certs generated by `make generate-certs`
├── deidentifhir/ # Deidentifhir profile (mounted into cd-agent)
├── mtls/ # Compose overlay + per-agent application.yaml for mTLS
├── results/ # Baseline transfer status JSONs (asserted by check-status)
└── test-data/ # Downloaded FHIR test bundles (gitignored)The CI job (.github/workflows/build.yml::e2e-tests) runs as a parallel matrix. Every leg brings up only the services it needs and exercises one scenario:
| Leg | Extra services | Scenarios | Project YAMLs | Baseline |
|---|---|---|---|---|
basic-and-oauth2-fail | — | basic-fail, oauth2-fail (negative) | generated by yq from gics-consent-example.yaml | fail-fatal.json |
gics-consent | gics, gpas | gics-consent-example | cd-agent/projects/gics-consent-example.yaml | example.json |
mtls | gpas | mtls-fail, mtls | cd-agent/projects/mtls{-fail,}.yaml, rd-agent/projects/mtls.yaml | fail-with-fhir-consent.json, example-with-fhir-consent.json |
The downstream e2e-status job is the single required check on main — it depends on every matrix leg and is green iff all of them succeeded.
Scenario semantics
- Negative tests assert a phase, not just an exit code. Baselines live in
results/:fail-fatal.json— process never enumerates patients (cohort-stage failure)fail-with-fhir-consent.json— cohort + deidentification succeed, every bundle is skipped at send (TLS or auth failure on the inter-agent hop)example.json/example-with-fhir-consent.json— happy-path success counts
- mTLS leg. The
mtls/compose.yamloverlay replaces each agent'sapplication.yamlbind-mount with one that definesclient-auth: needplusauth.client-cert.users, so oauth2/basic auth on TCA + RDA is removed and inter-agent calls go through mTLS. The mtls success project uses the FHIR cohort selector to avoid pulling in gICS, while the negative project intentionally swaps in a truststore-only SSL bundle on the CDA→RDA bundle sender so the handshake fails per patient.
Running a leg locally
cd .github/test
make pull
make generate-certs start upload-test-data
make transfer-all PROJECT=gics-consent-example
make wait
make check-status RESULTS_FILE=example.json
check-resources.sh example.json "$LAST_UPDATED" # uses .github/scripts on PATH
make check-pseudonymizationFor the mTLS leg, layer the overlay on top:
docker compose -f compose.yaml -f mtls/compose.yaml up --wait
make transfer-with-fhir-consent-list PROJECT=mtls TEST_SET_SIZE=10Adding a scenario
- Drop a new project YAML in
cd-agent/projects/(andrd-agent/projects/if needed). - Add a baseline JSON under
results/capturing the expectedphase,sentBundles,skippedBundles, and resource counts. - Either extend an existing matrix leg with a new step (
if: matrix.id == '<leg>') or add a new entry to thematrix.includeblock, listing the services / log buckets it needs. Avoid duplicating coverage already provided by another leg.
Coverage
Code coverage is collected automatically in CI. The patch diff should be 100%.
# Generate an aggregate coverage report
mvn jacoco:report-aggregate@reportUnit Tests
Unit tests verify isolated business logic without Spring context or external services.
Conventions
- Suffix:
*Test.java - Use AssertJ for assertions
- Use Mockito for mocking dependencies
- Test classes are package-private (no
publicmodifier)
Example
class EverythingDataSelectorConfigTest {
private static final HttpClientConfig FHIR_SERVER =
new HttpClientConfig("http://localhost");
@Test
void nullPageSizeUsesDefault() {
assertThat(new EverythingDataSelectorConfig(FHIR_SERVER, null))
.extracting(EverythingDataSelectorConfig::pageSize)
.isEqualTo(DEFAULT_PAGE_SIZE);
}
}Integration Tests
Integration tests verify components against mocked external services using WireMock within a Spring Boot context.
Conventions
- Suffix:
*IT.java - Annotated with
@SpringBootTestand@WireMockTest - Use
MockServerUtilfor building WireMock responses - Use
StepVerifierfrom Reactor Test for reactive assertions
Example
@SpringBootTest
@WireMockTest
class TcaCohortSelectorIT {
@Autowired MeterRegistry meterRegistry;
private WireMock wireMock;
private static TcaCohortSelector cohortSelector;
private static MockCohortSelector allCohortSelector;
@BeforeEach
void setUp(WireMockRuntimeInfo wireMockRuntime,
@Autowired WebClientFactory clientFactory) {
var config = new TcaCohortSelectorConfig(/* ... */);
cohortSelector = new TcaCohortSelector(
config,
clientFactory.create(clientConfig(wireMockRuntime)),
meterRegistry);
wireMock = wireMockRuntime.getWireMock();
allCohortSelector = MockCohortSelector.fetchAll(wireMock);
}
@Test
void consentBundleSucceeds() {
allCohortSelector.consentForOnePatient("patient");
create(cohortSelector.selectCohort(List.of()))
.expectNextCount(1)
.verifyComplete();
}
}Connection Scenario Tests
Extend AbstractConnectionScenarioIT as a @Nested class inside your integration test to automatically run six resilience scenarios: connection reset, timeout, first request fails, first and second fail, all fail, and wrong content type.
@Nested
public class FetchAllRequest extends AbstractConnectionScenarioIT {
@Override
protected TestStep<?> createTestStep() {
return new TestStep<ConsentedPatient>() {
@Override
public MappingBuilder requestBuilder() {
return post("/api/v2/cd/consented-patients/fetch-all");
}
@Override
public Flux<ConsentedPatient> executeStep() {
return cohortSelector.selectCohort(List.of());
}
};
}
}Authentication Tests
Extend AbstractAuthIT to verify that endpoints handle authentication correctly. The base class provides six tests covering public/protected endpoints with correct, incorrect, and missing credentials. OAuth2 tests require a running Keycloak instance. Start one with:
docker compose -f .github/test/oauth2/compose.yaml up --wait@Nested
@ActiveProfiles("auth_basic")
class BasicAuthIT extends AbstractAuthIT {
@Override
protected RequestHeadersSpec<?> protectedEndpoint(WebClient client) {
return client.post().uri("/api/v2/cd/consented-patients/fetch-all");
}
}Agent E2E Tests
Agent E2E tests run the actual agent inside a Docker container alongside WireMock containers for its dependencies, connected via a Docker network.
Conventions
- Suffix:
*E2E.java - Located in
src/e2e/java/ - Activated via the Maven profile
-Pe2e - Extend abstract base classes per agent (e.g.,
AbstractCohortSelectorE2E) - Use Testcontainers for container lifecycle management
Example
public class TCACohortSelectorE2E extends AbstractCohortSelectorE2E {
public TCACohortSelectorE2E() {
super("gics-consent-example.yaml");
}
@Override
protected void setupSpecificTcaMocks() {
var tcaWireMock = new WireMock(tca.getHost(), tca.getPort());
var cohortGenerator = createCohortGenerator(
"https://ths-greifswald.de/fhir/gics/identifiers/Pseudonym");
var tcaResponse = new Bundle()
.setEntry(List.of(new BundleEntryComponent()
.setResource(cohortGenerator.generate())));
tcaWireMock.register(
post(urlPathMatching("/api/v2/cd/consented-patients.*"))
.withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON_VALUE))
.willReturn(fhirResponse(tcaResponse)));
}
@Test
void testStartTransferAllProcessWithExampleProject() {
executeTransferTest("[]");
}
}Test Utilities
The test-util module provides shared testing infrastructure used across all agents. Key components:
| Class | Purpose |
|---|---|
MockServerUtil | WireMock response builders for FHIR and JSON responses, sequential mock scenarios |
FhirGenerators | Template-based FHIR resource factory for generating test data |
FhirGenerator | Reads JSON templates and replaces $PLACEHOLDER tokens with supplied values |
AbstractAuthIT | Base class providing six authentication test scenarios |
AbstractConnectionScenarioIT | Base class providing six connection resilience test scenarios |
TestWebClientFactory | Spring test component providing pre-configured WebClient instances |