Testing and Development Tools

Section 15 of 18
83% complete

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

ToolPurpose
JUnit 5Unit and integration testing framework
MockitoMocking framework for unit tests
TestcontainersDocker containers for integration tests
JavaFakerGenerate realistic test data
HAPI FHIR ServerLocal FHIR server for testing
AssertJFluent assertions library

Public Test Servers

ServerURL
HAPI FHIR R4http://hapi.fhir.org/baseR4
HAPI FHIR R5http://hapi.fhir.org/baseR5
Logica Sandboxhttps://api.logicahealth.org/fhirR4
SMART Health IThttps://r4.smarthealthit.org

Explore testing strategies and tools for healthcare applications:

Quiz: Testing and Development Tools

Question 1 of 5

Which Docker image runs a HAPI FHIR server?