|
| 1 | +# Testing |
| 2 | + |
| 3 | +FTSnext uses a multi-layer testing strategy to ensure correctness at different levels of |
| 4 | +abstraction. This page explains the test types, how to run them, and the conventions to follow |
| 5 | +when writing new tests. |
| 6 | + |
| 7 | +## Test Types |
| 8 | + |
| 9 | +| Type | Suffix | Location | Runs With | |
| 10 | +|---|---|---|---| |
| 11 | +| Unit | `*Test.java` | `src/test/java/` | `mvn test` | |
| 12 | +| Integration | `*IT.java` | `src/test/java/` | `mvn verify` | |
| 13 | +| Agent E2E | `*E2E.java` | `src/e2e/java/` | `mvn verify -Pe2e` | |
| 14 | +| E2E | Shell scripts | `.github/test/` | `make` (Docker Compose) | |
| 15 | + |
| 16 | +## Running Tests |
| 17 | + |
| 18 | +### Unit Tests |
| 19 | + |
| 20 | +```bash |
| 21 | +# Run all unit tests |
| 22 | +mvn clean test |
| 23 | + |
| 24 | +# Run a specific test class |
| 25 | +mvn test -Dtest=EverythingDataSelectorConfigTest |
| 26 | + |
| 27 | +# Run a single test method |
| 28 | +mvn test -Dtest=EverythingDataSelectorConfigTest#nullPageSizeUsesDefault |
| 29 | +``` |
| 30 | + |
| 31 | +### Integration Tests |
| 32 | + |
| 33 | +```bash |
| 34 | +# Run all unit and integration tests |
| 35 | +mvn clean verify |
| 36 | + |
| 37 | +# Run tests for a specific agent |
| 38 | +mvn clean verify --projects clinical-domain-agent --also-make |
| 39 | +``` |
| 40 | + |
| 41 | +### Building Docker Images |
| 42 | + |
| 43 | +Both agent E2E and E2E tests require locally built Docker images. Build them from the project |
| 44 | +root with: |
| 45 | + |
| 46 | +```bash |
| 47 | +make |
| 48 | +``` |
| 49 | + |
| 50 | +### Agent E2E Tests |
| 51 | + |
| 52 | +Agent E2E tests run against real Docker containers using Testcontainers. |
| 53 | + |
| 54 | +```bash |
| 55 | +# Run all agent E2E tests for a specific agent |
| 56 | +mvn clean verify -Pe2e --projects clinical-domain-agent --also-make |
| 57 | + |
| 58 | +# Run a specific E2E test |
| 59 | +mvn clean verify -Pe2e -Dit.test=TCACohortSelectorE2E \ |
| 60 | + --projects clinical-domain-agent --also-make |
| 61 | +``` |
| 62 | + |
| 63 | +### E2E Test |
| 64 | + |
| 65 | +The full end-to-end test starts all agents with their external dependencies (Blaze FHIR servers, |
| 66 | +gICS, gPAS, Keycloak, Redis) via Docker Compose. |
| 67 | + |
| 68 | +```bash |
| 69 | +cd .github/test |
| 70 | +make generate-certs start upload-test-data |
| 71 | +make transfer-all PROJECT=gics-consent-example |
| 72 | +make wait |
| 73 | +make check-status RESULTS_FILE=example.json |
| 74 | +make check-resources check-pseudonymization |
| 75 | +``` |
| 76 | + |
| 77 | +### Coverage |
| 78 | + |
| 79 | +Code coverage is collected automatically in CI. The patch diff should be 100%. |
| 80 | + |
| 81 | +```bash |
| 82 | +# Generate an aggregate coverage report |
| 83 | +mvn jacoco:report-aggregate@report |
| 84 | +``` |
| 85 | + |
| 86 | +## Unit Tests |
| 87 | + |
| 88 | +Unit tests verify isolated business logic without Spring context or external services. |
| 89 | + |
| 90 | +### Conventions |
| 91 | + |
| 92 | +- Suffix: `*Test.java` |
| 93 | +- Use [AssertJ][assertj] for assertions |
| 94 | +- Use Mockito for mocking dependencies |
| 95 | +- Test classes are package-private (no `public` modifier) |
| 96 | + |
| 97 | +### Example |
| 98 | + |
| 99 | +```java |
| 100 | +class EverythingDataSelectorConfigTest { |
| 101 | + |
| 102 | + private static final HttpClientConfig FHIR_SERVER = |
| 103 | + new HttpClientConfig("http://localhost"); |
| 104 | + |
| 105 | + @Test |
| 106 | + void nullPageSizeUsesDefault() { |
| 107 | + assertThat(new EverythingDataSelectorConfig(FHIR_SERVER, null)) |
| 108 | + .extracting(EverythingDataSelectorConfig::pageSize) |
| 109 | + .isEqualTo(DEFAULT_PAGE_SIZE); |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +## Integration Tests |
| 115 | + |
| 116 | +Integration tests verify components against mocked external services using [WireMock][wiremock] |
| 117 | +within a Spring Boot context. |
| 118 | + |
| 119 | +### Conventions |
| 120 | + |
| 121 | +- Suffix: `*IT.java` |
| 122 | +- Annotated with `@SpringBootTest` and `@WireMockTest` |
| 123 | +- Use `MockServerUtil` for building WireMock responses |
| 124 | +- Use `StepVerifier` from Reactor Test for reactive assertions |
| 125 | + |
| 126 | +### Example |
| 127 | + |
| 128 | +```java |
| 129 | +@SpringBootTest |
| 130 | +@WireMockTest |
| 131 | +class TcaCohortSelectorIT { |
| 132 | + |
| 133 | + @Autowired MeterRegistry meterRegistry; |
| 134 | + private WireMock wireMock; |
| 135 | + private static TcaCohortSelector cohortSelector; |
| 136 | + private static MockCohortSelector allCohortSelector; |
| 137 | + |
| 138 | + @BeforeEach |
| 139 | + void setUp(WireMockRuntimeInfo wireMockRuntime, |
| 140 | + @Autowired WebClientFactory clientFactory) { |
| 141 | + var config = new TcaCohortSelectorConfig(/* ... */); |
| 142 | + cohortSelector = new TcaCohortSelector( |
| 143 | + config, |
| 144 | + clientFactory.create(clientConfig(wireMockRuntime)), |
| 145 | + meterRegistry); |
| 146 | + wireMock = wireMockRuntime.getWireMock(); |
| 147 | + allCohortSelector = MockCohortSelector.fetchAll(wireMock); |
| 148 | + } |
| 149 | + |
| 150 | + @Test |
| 151 | + void consentBundleSucceeds() { |
| 152 | + allCohortSelector.consentForOnePatient("patient"); |
| 153 | + create(cohortSelector.selectCohort(List.of())) |
| 154 | + .expectNextCount(1) |
| 155 | + .verifyComplete(); |
| 156 | + } |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +### Connection Scenario Tests |
| 161 | + |
| 162 | +Extend `AbstractConnectionScenarioIT` as a `@Nested` class inside your integration test to |
| 163 | +automatically run six resilience scenarios: connection reset, timeout, first request fails, |
| 164 | +first and second fail, all fail, and wrong content type. |
| 165 | + |
| 166 | +```java |
| 167 | +@Nested |
| 168 | +public class FetchAllRequest extends AbstractConnectionScenarioIT { |
| 169 | + @Override |
| 170 | + protected TestStep<?> createTestStep() { |
| 171 | + return new TestStep<ConsentedPatient>() { |
| 172 | + @Override |
| 173 | + public MappingBuilder requestBuilder() { |
| 174 | + return post("/api/v2/cd/consented-patients/fetch-all"); |
| 175 | + } |
| 176 | + |
| 177 | + @Override |
| 178 | + public Flux<ConsentedPatient> executeStep() { |
| 179 | + return cohortSelector.selectCohort(List.of()); |
| 180 | + } |
| 181 | + }; |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +### Authentication Tests |
| 187 | + |
| 188 | +Extend `AbstractAuthIT` to verify that endpoints handle authentication correctly. The base class |
| 189 | +provides six tests covering public/protected endpoints with correct, incorrect, and missing |
| 190 | +credentials. OAuth2 tests require a running Keycloak instance. Start one with: |
| 191 | + |
| 192 | +```bash |
| 193 | +docker compose -f .github/test/oauth2/compose.yaml up --build --wait |
| 194 | +``` |
| 195 | + |
| 196 | +```java |
| 197 | +@Nested |
| 198 | +@ActiveProfiles("auth_basic") |
| 199 | +class BasicAuthIT extends AbstractAuthIT { |
| 200 | + @Override |
| 201 | + protected RequestHeadersSpec<?> protectedEndpoint(WebClient client) { |
| 202 | + return client.post().uri("/api/v2/cd/consented-patients/fetch-all"); |
| 203 | + } |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +## Agent E2E Tests |
| 208 | + |
| 209 | +Agent E2E tests run the actual agent inside a Docker container alongside WireMock containers for |
| 210 | +its dependencies, connected via a Docker network. |
| 211 | + |
| 212 | +### Conventions |
| 213 | + |
| 214 | +- Suffix: `*E2E.java` |
| 215 | +- Located in `src/e2e/java/` |
| 216 | +- Activated via the Maven profile `-Pe2e` |
| 217 | +- Extend abstract base classes per agent (e.g., `AbstractCohortSelectorE2E`) |
| 218 | +- Use Testcontainers for container lifecycle management |
| 219 | + |
| 220 | +### Example |
| 221 | + |
| 222 | +```java |
| 223 | +public class TCACohortSelectorE2E extends AbstractCohortSelectorE2E { |
| 224 | + |
| 225 | + public TCACohortSelectorE2E() { |
| 226 | + super("gics-consent-example.yaml"); |
| 227 | + } |
| 228 | + |
| 229 | + @Override |
| 230 | + protected void setupSpecificTcaMocks() { |
| 231 | + var tcaWireMock = new WireMock(tca.getHost(), tca.getPort()); |
| 232 | + var cohortGenerator = createCohortGenerator( |
| 233 | + "https://ths-greifswald.de/fhir/gics/identifiers/Pseudonym"); |
| 234 | + var tcaResponse = new Bundle() |
| 235 | + .setEntry(List.of(new BundleEntryComponent() |
| 236 | + .setResource(cohortGenerator.generate()))); |
| 237 | + |
| 238 | + tcaWireMock.register( |
| 239 | + post(urlPathMatching("/api/v2/cd/consented-patients.*")) |
| 240 | + .withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON_VALUE)) |
| 241 | + .willReturn(fhirResponse(tcaResponse))); |
| 242 | + } |
| 243 | + |
| 244 | + @Test |
| 245 | + void testStartTransferAllProcessWithExampleProject() { |
| 246 | + executeTransferTest("[]"); |
| 247 | + } |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +## Test Utilities |
| 252 | + |
| 253 | +The `test-util` module provides shared testing infrastructure used across all agents. Key |
| 254 | +components: |
| 255 | + |
| 256 | +| Class | Purpose | |
| 257 | +|---|---| |
| 258 | +| `MockServerUtil` | WireMock response builders for FHIR and JSON responses, sequential mock scenarios | |
| 259 | +| `FhirGenerators` | Template-based FHIR resource factory for generating test data | |
| 260 | +| `FhirGenerator` | Reads JSON templates and replaces `$PLACEHOLDER` tokens with supplied values | |
| 261 | +| `AbstractAuthIT` | Base class providing six authentication test scenarios | |
| 262 | +| `AbstractConnectionScenarioIT` | Base class providing six connection resilience test scenarios | |
| 263 | +| `TestWebClientFactory` | Spring test component providing pre-configured WebClient instances | |
| 264 | + |
| 265 | +[assertj]: https://assertj.github.io/doc/ |
| 266 | + |
| 267 | +[wiremock]: https://wiremock.org/ |
0 commit comments