Setting Up Test Server with Docker
Run a local HAPI FHIR server for development and testing. Docker containers provide isolated, reproducible environments that match production configurations. The official HAPI FHIR server image includes a complete FHIR R4 implementation with persistence, making it ideal for development and integration testing.
Docker Compose Configuration
Docker Compose simplifies multi-container setups and persists data between restarts. Configure environment variables to enable features like external references and multiple deletes for testing scenarios. Mount volumes for data persistence during development.
# docker-compose.yml
version: '3.8'
services:
hapi-fhir:
image: hapiproject/hapi:latest
ports:
- "8080:8080"
environment:
- hapi.fhir.fhir_version=R4
- hapi.fhir.server_address=http://localhost:8080/fhir
- hapi.fhir.allow_external_references=true
- hapi.fhir.allow_multiple_delete=true
- hapi.fhir.allow_placeholder_references=true
volumes:
- hapi-data:/data/hapi
volumes:
hapi-data:
Starting the Server
Use Docker Compose commands to manage the FHIR server lifecycle. Running in detached mode (-d) allows the server to continue running after you close the terminal. Check logs to monitor server startup and troubleshoot issues.
# Start the server
docker-compose up -d
# Check logs
docker-compose logs -f hapi-fhir
# Stop the server
docker-compose down
# Stop and remove data
docker-compose down -v
Unit Testing with HAPI FHIR
Write unit tests using JUnit 5 and Mockito. Unit tests verify individual components in isolation by mocking external dependencies like the FHIR client. Test resource creation, parsing, validation, and transformation logic without network calls. Unit tests run quickly and provide fast feedback during development.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class FhirServiceTest {
private FhirContext ctx;
@Mock
private IGenericClient mockClient;
@BeforeEach
public void setUp() {
ctx = FhirContext.forR4();
}
@Test
public void testCreatePatient() {
// Arrange
Patient patient = new Patient();
patient.addName().setFamily("Test").addGiven("Patient");
MethodOutcome outcome = new MethodOutcome();
outcome.setId(new IdType("Patient", "123"));
outcome.setCreated(true);
when(mockClient.create()
.resource(any(Patient.class))
.execute())
.thenReturn(outcome);
// Act
MethodOutcome result = mockClient.create()
.resource(patient)
.execute();
// Assert
assertNotNull(result.getId());
assertEquals("123", result.getId().getIdPart());
assertTrue(result.getCreated());
verify(mockClient.create()).resource(any(Patient.class));
}
@Test
public void testSearchPatients() {
// Arrange
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.SEARCHSET);
bundle.setTotal(1);
Patient patient = new Patient();
patient.setId("123");
patient.addName().setFamily("Smith");
bundle.addEntry()
.setResource(patient)
.setFullUrl("Patient/123");
when(mockClient.search()
.forResource(Patient.class)
.where(Patient.FAMILY.matches().value("Smith"))
.returnBundle(Bundle.class)
.execute())
.thenReturn(bundle);
// Act
Bundle results = mockClient.search()
.forResource(Patient.class)
.where(Patient.FAMILY.matches().value("Smith"))
.returnBundle(Bundle.class)
.execute();
// Assert
assertEquals(1, results.getTotal());
assertEquals(1, results.getEntry().size());
Patient resultPatient = (Patient) results.getEntry().get(0).getResource();
assertEquals("Smith", resultPatient.getNameFirstRep().getFamily());
}
@Test
public void testValidatePatient() {
// Test validation
Patient patient = new Patient();
patient.addName().setFamily("Test");
patient.setGender(Enumerations.AdministrativeGender.MALE);
FhirValidator validator = ctx.newValidator();
ValidationResult result = validator.validateWithResult(patient);
assertTrue(result.isSuccessful());
}
@Test
public void testParsePatientJson() {
String json = """
{
"resourceType": "Patient",
"id": "123",
"name": [{"family": "Test", "given": ["John"]}],
"gender": "male"
}
""";
IParser parser = ctx.newJsonParser();
Patient patient = parser.parseResource(Patient.class, json);
assertEquals("123", patient.getId());
assertEquals("Test", patient.getNameFirstRep().getFamily());
assertEquals(Enumerations.AdministrativeGender.MALE, patient.getGender());
}
}
Integration Testing with Testcontainers
Run integration tests against a real FHIR server. Testcontainers spins up Docker containers on demand during test execution, providing real infrastructure without manual setup. Integration tests verify complete workflows including network communication, data persistence, and server-side validation. Order tests using @TestMethodOrder to ensure dependent operations run sequentially.
import org.junit.jupiter.api.*;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FhirIntegrationTest {
@Container
private static final GenericContainer<?> hapiServer =
new GenericContainer<>("hapiproject/hapi:latest")
.withExposedPorts(8080)
.withEnv("hapi.fhir.fhir_version", "R4");
private static IGenericClient client;
private static FhirContext ctx;
private static String patientId;
@BeforeAll
public static void setUp() {
ctx = FhirContext.forR4();
String serverBase = "http://" +
hapiServer.getHost() + ":" +
hapiServer.getFirstMappedPort() + "/fhir";
client = ctx.newRestfulGenericClient(serverBase);
// Wait for server to be ready
waitForServerReady();
}
private static void waitForServerReady() {
int maxAttempts = 30;
for (int i = 0; i < maxAttempts; i++) {
try {
client.capabilities().ofType(CapabilityStatement.class).execute();
return;
} catch (Exception e) {
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
throw new RuntimeException("Server did not become ready");
}
@Test
@Order(1)
public void testCreatePatient() {
Patient patient = new Patient();
patient.addName()
.setFamily("IntegrationTest")
.addGiven("John");
patient.setGender(Enumerations.AdministrativeGender.MALE);
patient.setBirthDate(new Date());
MethodOutcome outcome = client.create()
.resource(patient)
.execute();
assertNotNull(outcome.getId());
assertTrue(outcome.getCreated());
patientId = outcome.getId().getIdPart();
System.out.println("Created patient: " + patientId);
}
@Test
@Order(2)
public void testReadPatient() {
assertNotNull(patientId, "Patient must be created first");
Patient patient = client.read()
.resource(Patient.class)
.withId(patientId)
.execute();
assertNotNull(patient);
assertEquals("IntegrationTest", patient.getNameFirstRep().getFamily());
}
@Test
@Order(3)
public void testSearchPatient() {
Bundle results = client.search()
.forResource(Patient.class)
.where(Patient.FAMILY.matches().value("IntegrationTest"))
.returnBundle(Bundle.class)
.execute();
assertTrue(results.getTotal() > 0);
boolean found = false;
for (Bundle.BundleEntryComponent entry : results.getEntry()) {
Patient p = (Patient) entry.getResource();
if (p.getId().endsWith(patientId)) {
found = true;
break;
}
}
assertTrue(found, "Created patient should be in search results");
}
@Test
@Order(4)
public void testUpdatePatient() {
Patient patient = client.read()
.resource(Patient.class)
.withId(patientId)
.execute();
patient.addTelecom()
.setSystem(ContactPoint.ContactPointSystem.EMAIL)
.setValue("[email protected]");
MethodOutcome outcome = client.update()
.resource(patient)
.execute();
assertNotNull(outcome.getId());
// Verify update
Patient updated = client.read()
.resource(Patient.class)
.withId(patientId)
.execute();
assertTrue(updated.hasTelecom());
assertEquals("[email protected]", updated.getTelecomFirstRep().getValue());
}
@Test
@Order(5)
public void testCreateObservation() {
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setSubject(new Reference("Patient/" + patientId));
obs.setCode(new CodeableConcept()
.addCoding()
.setSystem("http://loinc.org")
.setCode("8480-6")
.setDisplay("Systolic blood pressure"));
obs.setValue(new Quantity()
.setValue(120)
.setUnit("mmHg"));
MethodOutcome outcome = client.create()
.resource(obs)
.execute();
assertNotNull(outcome.getId());
assertTrue(outcome.getCreated());
}
@Test
@Order(6)
public void testTransaction() {
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
// Create patient
Patient patient = new Patient();
patient.addName().setFamily("Transaction").addGiven("Test");
bundle.addEntry()
.setFullUrl("urn:uuid:patient-1")
.setResource(patient)
.getRequest()
.setMethod(Bundle.HTTPVerb.POST)
.setUrl("Patient");
// Create observation
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setSubject(new Reference("urn:uuid:patient-1"));
obs.setCode(new CodeableConcept().setText("Test"));
bundle.addEntry()
.setResource(obs)
.getRequest()
.setMethod(Bundle.HTTPVerb.POST)
.setUrl("Observation");
Bundle response = client.transaction()
.withBundle(bundle)
.execute();
assertEquals(2, response.getEntry().size());
for (Bundle.BundleEntryComponent entry : response.getEntry()) {
assertTrue(entry.getResponse().getStatus().startsWith("201"));
}
}
@Test
@Order(7)
public void testDeletePatient() {
assertNotNull(patientId);
client.delete()
.resourceById("Patient", patientId)
.execute();
// Verify deletion
assertThrows(ResourceNotFoundException.class, () -> {
client.read()
.resource(Patient.class)
.withId(patientId)
.execute();
});
}
}
Test Data Generation
Generate realistic test data using JavaFaker. Realistic test data improves test coverage by exercising code paths with varied inputs. JavaFaker provides locale-aware generators for names, addresses, phone numbers, and other common fields. Combine with random clinical values to create comprehensive patient datasets for load testing and demos.
import com.github.javafaker.Faker;
import java.util.*;
public class TestDataGenerator {
private static final Faker faker = new Faker();
private static final Random random = new Random();
public static Patient generateRandomPatient() {
Patient patient = new Patient();
// Generate name
boolean isMale = random.nextBoolean();
patient.addName()
.setFamily(faker.name().lastName())
.addGiven(faker.name().firstName())
.setUse(HumanName.NameUse.OFFICIAL);
// Set gender
patient.setGender(isMale ?
Enumerations.AdministrativeGender.MALE :
Enumerations.AdministrativeGender.FEMALE);
// Generate birth date (between 18 and 90 years ago)
int yearsAgo = 18 + random.nextInt(72);
Calendar cal = Calendar.getInstance();
cal.add(Calendar.YEAR, -yearsAgo);
patient.setBirthDate(cal.getTime());
// Add identifier
patient.addIdentifier()
.setSystem("http://hospital.org/mrn")
.setValue("MRN-" + faker.number().digits(8));
// Add address
patient.addAddress()
.addLine(faker.address().streetAddress())
.setCity(faker.address().city())
.setState(faker.address().stateAbbr())
.setPostalCode(faker.address().zipCode())
.setCountry("USA");
// Add telecom
patient.addTelecom()
.setSystem(ContactPoint.ContactPointSystem.PHONE)
.setValue(faker.phoneNumber().phoneNumber());
patient.addTelecom()
.setSystem(ContactPoint.ContactPointSystem.EMAIL)
.setValue(faker.internet().emailAddress());
patient.setActive(true);
return patient;
}
public static Observation generateRandomObservation(String patientId) {
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setSubject(new Reference("Patient/" + patientId));
// Random vital sign
String[] vitalSigns = {
"8480-6|Systolic blood pressure",
"8462-4|Diastolic blood pressure",
"8867-4|Heart rate",
"9279-1|Respiratory rate",
"8310-5|Body temperature"
};
String selectedVital = vitalSigns[random.nextInt(vitalSigns.length)];
String[] parts = selectedVital.split("\\|");
obs.setCode(new CodeableConcept()
.addCoding()
.setSystem("http://loinc.org")
.setCode(parts[0])
.setDisplay(parts[1]));
// Generate appropriate value
double value;
String unit;
switch (parts[0]) {
case "8480-6": // Systolic
value = 90 + random.nextInt(60);
unit = "mmHg";
break;
case "8462-4": // Diastolic
value = 60 + random.nextInt(40);
unit = "mmHg";
break;
case "8867-4": // Heart rate
value = 60 + random.nextInt(100);
unit = "beats/minute";
break;
case "9279-1": // Respiratory rate
value = 12 + random.nextInt(13);
unit = "breaths/minute";
break;
default: // Temperature
value = 36.0 + (random.nextDouble() * 2);
unit = "Cel";
}
obs.setValue(new Quantity()
.setValue(value)
.setUnit(unit)
.setSystem("http://unitsofmeasure.org"));
// Set effective date/time
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, -random.nextInt(365));
obs.setEffective(new DateTimeType(cal.getTime()));
return obs;
}
public static List<Patient> generatePatientDataset(int count) {
List<Patient> patients = new ArrayList<>();
for (int i = 0; i < count; i++) {
patients.add(generateRandomPatient());
}
return patients;
}
public static void main(String[] args) {
// Generate test data
FhirContext ctx = FhirContext.forR4();
IParser parser = ctx.newJsonParser().setPrettyPrint(true);
List<Patient> patients = generatePatientDataset(10);
for (Patient patient : patients) {
System.out.println(parser.encodeResourceToString(patient));
System.out.println("---");
}
}
}
Testing Tools Summary
| Tool | Purpose |
|---|---|
| JUnit 5 | Unit and integration testing framework |
| Mockito | Mocking framework for unit tests |
| Testcontainers | Docker containers for integration tests |
| JavaFaker | Generate realistic test data |
| HAPI FHIR Server | Local FHIR server for testing |
| AssertJ | Fluent assertions library |
Public Test Servers
| Server | URL |
|---|---|
| HAPI FHIR R4 | http://hapi.fhir.org/baseR4 |
| HAPI FHIR R5 | http://hapi.fhir.org/baseR5 |
| Logica Sandbox | https://api.logicahealth.org/fhirR4 |
| SMART Health IT | https://r4.smarthealthit.org |
Related Articles
Explore testing strategies and tools for healthcare applications:
- Unit Testing 101 For Non-Programmers - Testing fundamentals
- Why Testing is Important - Understanding the value of testing
- Automating PDF-related Structural Testing - Automated testing techniques
- Orthanc DICOM Server for Testing - Using Orthanc for healthcare app testing