Skip to content

Best Practices

Guidelines for effective FHIR testing with FHIR Frog.

TestScript Design

Keep Tests Focused

Single Responsibility

Each TestScript should test one specific behavior or requirement.

Good:

TestScript: PatientCreate
Test: "Create patient with valid data"
  * operation create Patient from validPatient
  * assert responseCode = 201

Avoid:

TestScript: PatientEverything
Test: "Create, read, update, delete, search, validate..."
  # Too many responsibilities

Use Meaningful Names

# Good
TestScript: PatientMedicareNumberValidation
TestScript: PractitionerHPIISearch

# Avoid
TestScript: Test1
TestScript: PatientTest

Document Test Intent

TestScript: PatientIHIValidation
Title: "Validate Patient with IHI identifier"
Description: """
  Verifies that a Patient resource with a valid IHI 
  (Individual Healthcare Identifier) can be created 
  and retrieved successfully.
"""

Test Organization

Use TestPlans to organize:

TestPlan: AUCorePatientSuite
  - PatientCreate
  - PatientRead
  - PatientUpdate
  - PatientDelete
  - PatientSearch

Separate Positive and Negative Tests

tests/
  patient/
    positive/
      create-valid.tsh
      search-by-identifier.tsh
    negative/
      create-invalid-gender.tsh
      create-missing-name.tsh

Use Fixtures Effectively

fixtures/
  patient/
    valid-patient.json
    patient-with-medicare.json
    patient-with-ihi.json
  invalid/
    patient-invalid-gender.json
    patient-missing-name.json

Variable Management

Extract IDs Early

Test: "Create and read patient"
  * operation create Patient from patient as createResponse
  * assert responseCode = 201
  * variable patientId = Patient.id from createResponse
  * operation read Patient/${patientId}
  * assert responseCode = 200

Use Descriptive Variable Names

# Good
* variable patientId = Patient.id
* variable medicareNumber = Patient.identifier.where(system='...').value

# Avoid
* variable id1 = Patient.id
* variable x = Patient.identifier.value

Validate Before Using

* operation create Patient from patient as response
* assert expression = "Patient.id.exists()"
* variable patientId = Patient.id from response

Assertion Strategies

Test Multiple Aspects

Test: "Create patient"
  * operation create Patient from patient
  * assert responseCode = 201
  * assert resource = Patient
  * assert expression = "Patient.id.exists()"
  * assert expression = "Patient.name.exists()"

Use FHIRPath for Complex Checks

# Check identifier system
* assert expression = "Patient.identifier.where(system='http://ns.electronichealth.net.au/id/medicare-number').exists()"

# Validate reference format
* assert expression = "Observation.subject.reference.startsWith('Patient/')"

Provide Clear Messages

* assert responseCode = 201
  description = "Patient creation must return 201 Created"

* assert expression = "Patient.name.family.exists()"
  description = "Patient must have a family name"

Error Handling

Clean Up on Failure

Setup:
  * operation create Patient from patient as setupResponse
  * variable patientId = Patient.id from setupResponse

Test: "Update patient"
  * operation update Patient/${patientId} from updatedPatient
  * assert responseCode = 200

Teardown:
  * operation delete Patient/${patientId}
  # Always runs, even if test fails

Use Warning-Only for Optional Checks

* assert expression = "Patient.telecom.where(system='email').exists()"
  warningOnly = true
  description = "Email is recommended but not required"

Performance

Minimize Server Calls

# Good - Single transaction
* operation transaction from patientBundle

# Avoid - Multiple calls
* operation create Patient from patient1
* operation create Patient from patient2
* operation create Patient from patient3

Reuse Setup Data

Setup:
  * operation create Patient from sharedPatient as setupResponse
  * variable sharedPatientId = Patient.id from setupResponse

Test: "Test 1"
  * operation read Patient/${sharedPatientId}

Test: "Test 2"
  * operation read Patient/${sharedPatientId}

CI/CD Integration

Fast Feedback

# Run critical tests first
test:critical:
  script:
    - mvn test -Dtest=CriticalTests

test:full:
  script:
    - mvn test
  needs: [test:critical]

Parallel Execution

test:patient:
  script:
    - mvn test -Dtest=PatientTests

test:practitioner:
  script:
    - mvn test -Dtest=PractitionerTests

Store Reports

artifacts:
  paths:
    - target/fhir-reports/
  expire_in: 30 days

Testcontainers

Use Specific Versions

@Container
static GenericContainer<?> fhirServer = new GenericContainer<>("hapiproject/hapi:v6.4.0")
    .withExposedPorts(8080);

Wait for Readiness

@Container
static GenericContainer<?> fhirServer = new GenericContainer<>("hapiproject/hapi:latest")
    .withExposedPorts(8080)
    .waitingFor(Wait.forHttp("/fhir/metadata")
        .forStatusCode(200)
        .withStartupTimeout(Duration.ofMinutes(2)));

Reuse Containers

@Testcontainers
class AllTests {
    @Container
    static GenericContainer<?> fhirServer = new GenericContainer<>(...)
        .withReuse(true);
}

Security

Avoid Hardcoded Credentials

// Good
@DynamicFhirServerUrl
static String getServerUrl() {
    return System.getenv("FHIR_SERVER_URL");
}

// Avoid
@FhirServerUrl("http://admin:password@server/fhir")

Sanitize Test Data

# Use fake data
Fixture: patient from "test-patient.json"
  # Contains: John Doe, [email protected], 555-0100

Don't Test Production

@EnabledIfEnvironmentVariable(named = "ENV", matches = "test|dev")
class IntegrationTests {}

Common Patterns

CRUD Pattern

Setup:
  * operation create Patient from patient as createResponse
  * variable patientId = Patient.id from createResponse

Test: "Read"
  * operation read Patient/${patientId}
  * assert responseCode = 200

Test: "Update"
  * operation update Patient/${patientId} from updatedPatient
  * assert responseCode = 200

Test: "Delete"
  * operation delete Patient/${patientId}
  * assert responseCode = 204

Search Pattern

Setup:
  * operation create Patient from searchablePatient

Test: "Search by name"
  * operation search Patient?name=Smith
  * assert responseCode = 200
  * assert expression = "Bundle.entry.count() > 0"

Validation Pattern

Test: "Validate valid resource"
  * operation validate Patient from validPatient
  * assert responseCode = 200
  * assert expression = "OperationOutcome.issue.where(severity='error').count() = 0"

Test: "Validate invalid resource"
  * operation validate Patient from invalidPatient
  * assert responseCode = 400
  * assert expression = "OperationOutcome.issue.where(severity='error').count() > 0"

Summary

Key Takeaways

  • Keep tests focused and well-named
  • Use fixtures and variables effectively
  • Clean up resources in teardown
  • Integrate with CI/CD pipelines
  • Follow security best practices

Next Steps