Skip to content

Commit bebdfd5

Browse files
committed
Add Testing Section to Contributing Docs
1 parent 6f81d64 commit bebdfd5

File tree

3 files changed

+269
-1
lines changed

3 files changed

+269
-1
lines changed

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export default withMermaid({
155155
text: 'Development', link: '/contributing/overview',
156156
items: [
157157
{text: 'Contributing', link: '/contributing/contributing'},
158+
{text: 'Testing', link: '/contributing/testing'},
158159
{text: 'Repository Structure', link: '/contributing/structure'},
159160
{text: 'Clinical Domain Agent', link: '/contributing/clinical-domain-agent'},
160161
{

docs/contributing/contributing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ details, code and code quality.*
4444
* **Code Quality**: CodeQL will be used for static code analysis. Ensure code passes these checks.
4545
* **Testing**: New code must be tested. Code coverage will be collected automatically, and
4646
the patch diff should be 100%. Maintainers may skip these requirements if there are valid reasons
47-
to do so.
47+
to do so. See the [Testing](testing) page for details on test types and how to run them.
4848
* **Code Readability**: Use comments to indicate hard-to-understand code snippets, but aim to name
4949
all entities so that the code is as human-readable as possible.
5050

docs/contributing/testing.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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

Comments
 (0)