12.1.1Tutorial: Writing Tests with SmileCdrContainer and SmileHarness

 

This tutorial will guide you through the process of writing tests that use SmileCdrContainer and SmileHarness to interact with a Smile CDR instance. We will walk through existing sample tests, explaining the behaviour.

12.1.1.1Prerequisites

Before you begin, make sure you have:

  • Java 17 or later installed
  • Maven installed
  • Docker installed and running
  • Access to the Smile CDR Docker repository, or a repository you host yourself which contains Smile CDR images.

12.1.1.2Step 1: Set Up Your Project

First, download the cdr-interceptor-starterproject zip file or tarball. This will provide you with a baseline test class that we will explore for the rest of this tutorial.

In your terminal, navigate to the project root, and run the following command to build the project locally.

mvn clean install -DskipTests

Once that is done, you are free to run the following command, which will run all the tests in the project.

mvn verify

Note that this may take some time. Once it is finished, you should see that maven reports that all tests are passing.

12.1.1.3Example 1: BoilerplateIT.java

To start, open BoilerplateIT.java, located in cdr-interceptor-starterproject/src/test/java/com/smilecdr/demo/examples/, in your preferred IDE. Let's walk through this sample test class, and explain what each part does, so you can start building your own tests.

package com.smilecdr.demo.examples;

import ca.cdr.test.extensions.SmileCdrContainer;
import ca.cdr.test.app.harness.api.SmileHarness;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

import static org.assertj.core.api.Assertions.assertThat;

@Testcontainers
class BoilerplateIT {

   @Container
   private static SmileCdrContainer container = new SmileCdrContainer();

   private static SmileHarness harness;

   @BeforeAll
   public static void setup() {
      harness = container.getHarness();
   }

   @Test
   void testSmileCdrIsRunning() {
      // This test will pass if the container starts successfully
      assertThat(container.isRunning()).isTrue();

      //Check that we can determine the FhirContext from the running server.
      assertThat(harness.getFhirContext().getVersion().getVersion().toString()).isEqualTo("R4");
   }
}

This test simply verifies that the Smile CDR container starts successfully, and that we can connect to it through the harness. When you run this test, it will:

  1. Download the Smile CDR Docker image (if not already present) 2. Start a container with the default configuration 3. Wait for the container to be ready 4. Run the test 5. Stop and remove the container when the test is complete

Here's a breakdown of some of the setup you see in this class:

  • @Testcontainers – This tells the testing framework to control the lifecycle of docker containers in this test.
  • @Container private SmileCdrContainer ourContainer; – This tells Testcontainers that we will be creating a SmileCdrContainer to be managed for the duration of this test class.
  • harness = conatiner.getHarness() – After it starts, we ask the SmileCdrContainer to give us a test harness, so that we can write tests which connect to the running Smile CDR container.
If you want more detailed reference information about these classes, check out the Test Tools Overview page

This test on its own is not very useful, so the remainder of this tutorial will be a walk through the existing test classes that live alongside BoilerplateIT.java

12.1.1.4Example 2: FhirIT.java

This file contains multiple tests related to interacting with the FHIR API on the running Smile CDR instance.

Starting out easy, the following test simply creates a Patient object, retrieves it, and validates that it has the name it was just given.

@Test
void testCreateAndRetrievePatientAsAdmin() {
   // Get a FHIR client with admin credentials
   IGenericClient fhirClient = harness.getSuperuserFhirClient();

   // Create a new patient
   Patient patient = new Patient();
   patient.addName().setFamily("Smith").addGiven("John");
   patient.setGender(Enumerations.AdministrativeGender.MALE);

   // Save the patient to the server
   patient = (Patient) fhirClient.create().resource(patient).execute().getResource();

   // Verify the patient was created with an ID
   assertThat(patient.getIdElement().getValue()).isNotNull();
   String patientId = patient.getIdElement().getIdPart();

   // Retrieve the patient by ID
   Patient retrievedPatient = fhirClient.read().resource(Patient.class).withId(patientId).execute();

   // Verify the retrieved patient matches what we created
   assertThat(retrievedPatient.getName().get(0).getFamily()).isEqualTo("Smith");
   assertThat(retrievedPatient.getName().get(0).getGiven().get(0).toString()).isEqualTo("John");
}

See that we ask our harness to provide us with a FHIR-compatible client by calling harness.getSuperuserFhirClient(). This method returns an IGenericClient which is capable of communicating with Smile CDR. If you are unfamiliar with this class, please read the IGenericClient documentation, as it will be used in all the tests in this class.

The harness allows you to fetch an IGenericClient in multiple ways. All of these are documented in the API Docs for SmileHarness;

12.1.1.4.1Transaction Bundle with Patient and Observations

The following test demonstrates how to create a patient and observations, and then use a transaction bundle to search for these resources:

@Test
void testTransactionBundleWithPatientAndObservations() {
   // Get a FHIR client with admin credentials
   IGenericClient fhirClient = harness.getSuperuserFhirClient();

   // Create a new patient
   Patient patient = new Patient();
   patient.addIdentifier().setSystem("http://example.org/mrn").setValue("12345");
   patient.addName().setFamily("Johnson").addGiven("Robert");
   patient.setGender(Enumerations.AdministrativeGender.MALE);

   // Save the patient to the server
   MethodOutcome patientOutcome = fhirClient.create().resource(patient).execute();
   assertThat(patientOutcome).isNotNull();
   assertThat(patientOutcome.getId()).isNotNull();
   String patientId = patientOutcome.getId().getIdPart();

   // Create two observations linked to the patient
   Observation observation1 = new Observation();
   observation1.setStatus(Observation.ObservationStatus.FINAL);
   observation1.getSubject().setReference("Patient/" + patientId);
   observation1.getCode().addCoding()
      .setSystem("http://loinc.org")
      .setCode("8867-4")
      .setDisplay("Heart rate");
   observation1.setValue(new Quantity().setValue(72).setUnit("beats/minute"));

   // Save the first observation
   MethodOutcome obs1Outcome = fhirClient.create().resource(observation1).execute();
   assertThat(obs1Outcome).isNotNull();
   assertThat(obs1Outcome.getId()).isNotNull();

   // Create and save the second observation
   Observation observation2 = new Observation();
   observation2.setStatus(Observation.ObservationStatus.FINAL);
   observation2.getSubject().setReference("Patient/" + patientId);
   observation2.getCode().addCoding()
      .setSystem("http://loinc.org")
      .setCode("8480-6")
      .setDisplay("Systolic blood pressure");
   observation2.setValue(new Quantity().setValue(120).setUnit("mm[Hg]"));

   MethodOutcome obs2Outcome = fhirClient.create().resource(observation2).execute();
   assertThat(obs2Outcome).isNotNull();
   assertThat(obs2Outcome.getId()).isNotNull();

   // Now demonstrate a transaction bundle that searches for these resources
   Bundle searchBundle = new Bundle();
   searchBundle.setType(Bundle.BundleType.TRANSACTION);

   // Add a search for the patient
   searchBundle.addEntry()
      .getRequest()
         .setMethod(HTTPVerb.GET)
         .setUrl("Patient?identifier=http://example.org/mrn|12345");

   // Add a search for observations related to this patient
   searchBundle.addEntry()
      .getRequest()
         .setMethod(HTTPVerb.GET)
         .setUrl("Observation?subject=Patient/" + patientId);
   // Execute the transaction
   Bundle responseBundle = fhirClient.transaction().withBundle(searchBundle).execute();

   // Verify the response
   assertThat(responseBundle).isNotNull();
   assertThat(responseBundle.getType()).isEqualTo(Bundle.BundleType.TRANSACTIONRESPONSE);
   assertThat(responseBundle.getEntry().size()).isEqualTo(2);

   // The first entry should contain the patient search results
   Bundle patientSearchResult = (Bundle) responseBundle.getEntry().get(0).getResource();
   assertThat(patientSearchResult).isNotNull();
   assertThat(patientSearchResult.getEntry().size()).isEqualTo(1);

   // The second entry should contain the observation search results
   Bundle obsSearchResult = (Bundle) responseBundle.getEntry().get(1).getResource();
   assertThat(obsSearchResult).isNotNull();
   assertThat(obsSearchResult.getEntry().size()).isEqualTo(2);
}

This test:

  1. Creates a patient with an identifier 2. Creates two observations linked to that patient 3. Creates a transaction bundle with search operations 4. Executes the transaction and verifies the results

12.1.1.4.2Conditional Create

The following test demonstrates conditional create functionality, which allows you to create a resource only if it doesn't already exist:

@Test
void testConditionalCreateBundle() {
   // Get a FHIR client with admin credentials
   IGenericClient fhirClient = harness.getSuperuserFhirClient();

   // Create a unique identifier for this test
   String patientIdentifierSystem = "http://example.org/mrn";
   String patientIdentifierValue = "67890-" + System.currentTimeMillis(); // Make it unique

   // Create a patient directly (not using conditional create)
   Patient patient = new Patient();
   patient.addIdentifier().setSystem(patientIdentifierSystem).setValue(patientIdentifierValue);
   patient.addName().setFamily("Davis").addGiven("Sarah");
   patient.setGender(Enumerations.AdministrativeGender.FEMALE);

   // Create the patient directly
   MethodOutcome outcome = fhirClient.create().resource(patient).execute();

   // Verify the outcome
   assertThat(outcome.getCreated()).isTrue();
   assertThat(outcome.getId()).isNotNull();
   String firstPatientId = outcome.getId().getIdPart();

   // Create a second patient with the same identifier but different name
   Patient patient2 = new Patient();
   patient2.addIdentifier().setSystem(patientIdentifierSystem).setValue(patientIdentifierValue);
   patient2.addName().setFamily("Davis").addGiven("Jane"); // Different given name
   patient2.setGender(Enumerations.AdministrativeGender.FEMALE);

   // Try to create it using conditional create
   MethodOutcome outcome2 = fhirClient.create()
      .resource(patient2)
      .conditional()
      .where(Patient.IDENTIFIER.exactly().systemAndCode(patientIdentifierSystem, patientIdentifierValue))
      .execute();

   // Verify the outcome - should return the same ID as the first patient, and the outcome should indicate
   // that there was no creation
   assertThat(outcome2.getId()).isNotNull();
   String secondPatientId = outcome2.getId().getIdPart();

   // The IDs should be the same since the conditional create should find the existing patient
   assertThat(secondPatientId).isEqualTo(firstPatientId);

   // Read the original patient directly to verify it still has the original name
   Patient retrievedPatient = fhirClient.read().resource(Patient.class).withId(firstPatientId).execute();
   assertThat(retrievedPatient).isNotNull();
   assertThat(retrievedPatient.getName().get(0).getGiven().get(0).toString()).isEqualTo("Sarah");
}

This test:

  1. Creates a patient with a unique identifier 2. Attempts to create another patient with the same identifier using conditional create 3. Verifies that the second create operation didn't create a new resource 4. Confirms the original patient data is unchanged

12.1.1.4.3Create and Delete Observation

The following test demonstrates how to create and delete a FHIR resource:

@Test
void testCreateAndDeleteObservation() {
   // Get a FHIR client with admin credentials
   IGenericClient fhirClient = harness.getSuperuserFhirClient();

   // Create a new observation
   Observation observation = new Observation();
   observation.setStatus(Observation.ObservationStatus.FINAL);
   observation.getCode().addCoding()
      .setSystem("http://loinc.org")
      .setCode("8310-5")
      .setDisplay("Body temperature");
   observation.setValue(new Quantity().setValue(37.0).setUnit("C"));

   // Save the observation to the server
   MethodOutcome outcome = fhirClient.create().resource(observation).execute();

   // Verify the observation was created with an ID
   assertThat(outcome.getId()).isNotNull();
   String observationId = outcome.getId().getIdPart();

   // Retrieve the observation by ID to confirm it exists
   Observation retrievedObservation = fhirClient.read().resource(Observation.class).withId(observationId).execute();
   assertThat(retrievedObservation).isNotNull();

   // Delete the observation
   fhirClient.delete().resourceById("Observation", observationId).execute();

   // Try to retrieve the observation again - should throw ResourceGoneException
   try {
      fhirClient.read().resource(Observation.class).withId(observationId).execute();
      // If we get here, the test should fail
      fail("Expected ResourceGoneException was not thrown");
   } catch (Exception e) {
      // Expected exception - observation should be gone
      assertThat(e.getMessage()).contains("HTTP 410 Gone");
   }
}

This test:

  1. Creates an observation 2. Verifies it was created successfully 3. Deletes the observation 4. Verifies that the observation is no longer accessible

12.1.1.4.4Validate Operation

The following test demonstrates how to use the $validate operation to validate a FHIR resource:

@Test
void testValidateOperation() {
   // Get a FHIR client with admin credentials
   IGenericClient fhirClient = harness.getSuperuserFhirClient();

   // Create a patient
   Patient patient = new Patient();
   patient.addIdentifier().setSystem("http://example.org/mrn").setValue("54321");
   patient.addName().setFamily("Wilson").addGiven("James");
   patient.setGender(Enumerations.AdministrativeGender.MALE);

   // Save the patient to the server
   MethodOutcome createOutcome = fhirClient.create().resource(patient).execute();
   String patientId = createOutcome.getId().getIdPart();

   // Retrieve the patient

   // Validate the patient using the $validate operation

   Parameters outParams = fhirClient.operation()
      .onInstance(new IdType("Patient", patientId))
      .named("$validate")
      .withNoParameters(Parameters.class)
      .execute();

   // Extract the validation result
   OperationOutcome outcome = (OperationOutcome) outParams.getParameter().get(0).getResource();

   // Verify the validation was successful
   assertThat(outcome).isNotNull();
   assertThat(outcome.getIssue()).isNotEmpty();

   // Check if there are any errors
   boolean hasErrors = outcome.getIssue().stream()
      .anyMatch(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR);


   assertThat(hasErrors).isFalse();

   //Given that we haven't added a narrative, the default validation should warn about that!
   List<OperationOutcome.OperationOutcomeIssueComponent> warnings = outcome.getIssue().stream()
      .filter(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.WARNING)
         .collect(Collectors.toUnmodifiableList());

   assertThat(warnings).hasSize(1);
   assertThat(warnings.get(0).getDetails().getCodingFirstRep().getCode()).contains("dom-6");
}

This test:

  1. Creates a patient 2. Uses the $validate operation to validate the patient 3. Checks the validation results for errors and warnings

12.1.1.4.5Search with Include

The following test demonstrates how to use the _include parameter to include referenced resources in search results:

@Test
void testSearchWithInclude() {
   // Get a FHIR client with admin credentials
   IGenericClient fhirClient = harness.getSuperuserFhirClient();

   // Create a patient
   Patient patient = new Patient();
   patient.addIdentifier().setSystem("http://example.org/mrn").setValue("98765");
   patient.addName().setFamily("Brown").addGiven("Michael");
   MethodOutcome patientOutcome = fhirClient.create().resource(patient).execute();
   String patientId = patientOutcome.getId().getIdPart();

   // Create an observation linked to the patient
   Observation observation = new Observation();
   observation.setStatus(Observation.ObservationStatus.FINAL);
   observation.getSubject().setReference("Patient/" + patientId);
   observation.getCode().addCoding()
      .setSystem("http://loinc.org")
      .setCode("8302-2")
      .setDisplay("Body height");
   observation.setValue(new Quantity().setValue(180).setUnit("cm"));
   fhirClient.create().resource(observation).execute();

   // Search for observations and include the referenced patient
   Bundle searchResult = fhirClient.search()
      .forResource(Observation.class)
      .where(Observation.SUBJECT.hasId(patientId))
      .include(Observation.INCLUDE_PATIENT)
      .returnBundle(Bundle.class)
      .execute();

   // Verify we got both observation and patient resources
   assertThat(searchResult.getEntry().size()).isEqualTo(2);

   // Check that we have at least one Patient resource in the results
   boolean hasPatient = searchResult.getEntry().stream()
      .anyMatch(entry -> entry.getResource() instanceof Patient);

   assertThat(hasPatient).isTrue();

   // Check that we have at least one Observation resource in the results
   boolean hasObservation = searchResult.getEntry().stream()
      .anyMatch(entry -> entry.getResource() instanceof Observation);

   assertThat(hasObservation).isTrue();
}

This test:

  1. Creates a patient 2. Creates an observation that references the patient 3. Searches for observations and includes the referenced patient 4. Verifies that both the observation and patient are returned in the results

12.1.1.4.6Transaction Bundle with Multiple Patients

The following test demonstrates how to use a transaction bundle to create multiple resources in a single request:

@Test
void testTransactionBundleWithMultiplePatients() {
   // Get a FHIR client with admin credentials
   IGenericClient fhirClient = harness.getSuperuserFhirClient();

   // Create a transaction bundle
   Bundle transactionBundle = new Bundle();
   transactionBundle.setType(Bundle.BundleType.TRANSACTION);

   // Add 5 random patients to the bundle
   for (int i = 0; i < 5; i++) {
      // Create a new patient with random data
      Patient patient = new Patient();

      // Add a unique identifier
      String uniqueId = "patient-" + System.currentTimeMillis() + "-" + i;
      patient.addIdentifier().setSystem("http://example.org/patients").setValue(uniqueId);

      // Add the patient to the transaction bundle
      transactionBundle.addEntry()
         .setResource(patient)
         .getRequest()
            .setMethod(HTTPVerb.POST)
            .setUrl("Patient");
   }

   // Execute the transaction
   Bundle responseBundle = fhirClient.transaction().withBundle(transactionBundle).execute();

   // Verify the response
   assertThat(responseBundle).isNotNull();
   assertThat(responseBundle.getType()).isEqualTo(Bundle.BundleType.TRANSACTIONRESPONSE);
   assertThat(responseBundle.getEntry().size()).isEqualTo(5);

   // Verify we can search for the created patients
   Bundle searchBundle = fhirClient.search()
      .forResource(Patient.class)
      .where(Patient.IDENTIFIER.hasSystemWithAnyCode("http://example.org/patients"))
      .returnBundle(Bundle.class)
      .execute();

   // We should find exactly 5 patients with our system
   assertThat(searchBundle.getEntry().size()).isEqualTo(5);
}

This test:

  1. Creates a transaction bundle 2. Adds 5 patients to the bundle 3. Executes the transaction 4. Verifies all patients were created successfully

12.1.1.5Example 3: AdminJsonIT.java

Now, we move on from FHIR interactions, and start looking at some other functionality provided by SmileHarness, the AdminJsonRestClient. This client is built to interact with modules of type ADMIN_JSON. It has a multitude of convenience methods documented in the Javadocs.

12.1.1.5.1Get Node Configuration

@Test
void testGetNodeConfigurations() {
   // Get an Admin JSON client
   AdminJsonRestClient adminClient = harness.getAdminJsonClient();

   // Get the node configurations
   NodeConfigurations config = adminClient.getNodeConfigurations();

   // Verify we have at least one node
   assertThat(config.getNodes()).isNotEmpty();

   // Get the first node
   NodeConfigurations.NodeConfiguration node = config.getNodes().get(0);

   // Verify the node has the expected ID
   assertThat(node.getNodeId()).isEqualTo("Master");

   // Verify the node has modules
   assertThat(node.getModules()).isNotEmpty();

   // Find the FHIR endpoint module
   Optional<NodeConfigurations.ModuleConfiguration> fhirModule = node.getModule("fhir_endpoint");
   assertThat(fhirModule).isPresent();

   // Verify the FHIR endpoint port
   String portValue = fhirModule.get().getConfigProperty("port");
   assertThat(portValue).isEqualTo("8000");
}

This test:

  1. Gets an Admin JSON client 2. Retrieves the node configurations 3. Verifies the configuration contains the expected data

12.1.1.5.2Create User with Strong Password

@Test
public void testCreateUserWithStrongPasswordSucceeds() {
   // Get an Admin JSON client
   AdminJsonRestClient adminClient = harness.getAdminJsonClient();
   UserDetailsJson userDetailsJson = new UserDetailsJson();
   userDetailsJson.setUsername("a-test-user");
   userDetailsJson.setPassword("HJ!cJm9qO!tYzy");
   userDetailsJson.setEmail("an-email@address.com");

   //Ensure the user has no ID
   assertThat(userDetailsJson.getPid()).isNull();

   userDetailsJson = adminClient.userCreate("Master", "local_security", userDetailsJson);

   //Ensure the user has an ID now
   assertThat(userDetailsJson.getPid()).isNotNull();
}

This test:

  1. Gets an Admin JSON client 2. Creates a user with a strong password 3. Verifies the user was created successfully with a valid ID

12.1.1.5.3Create User with Weak Password

@Test
public void testCreateUserWithWeakPasswordFails() {
   // Get an Admin JSON client
   AdminJsonRestClient adminClient = harness.getAdminJsonClient();
   UserDetailsJson userDetailsJson = new UserDetailsJson();
   userDetailsJson.setUsername("test-user");
   userDetailsJson.setPassword("weakpassword");
   userDetailsJson.setEmail("email@address.com");

   try {
      adminClient.userCreate("Master", "local_security_in", userDetailsJson);
      fail();
   } catch (Exception e) {
      assertThat(e).hasMessageContaining("New password should have at least 3 character types including lowercase letters, uppercase letters, numbers, and symbols.");
   }
}

This test:

  1. Gets an Admin JSON client 2. Attempts to create a user with a weak password 3. Verifies that the creation fails with an appropriate error message about password strength

12.1.1.5.4Create User with Specific Authorities

@Test
public void testCreateUserWithSpecificAuthorities() {
   //Build a set of authorities that will allow a user to hit /metadata, abd /Observation
   GrantedAuthorityJson readAllObservationsPermission = new GrantedAuthorityJson(PermissionEnum.FHIR_READ_ALL_OF_TYPE, "Observation?");
   GrantedAuthorityJson metadataPermission = new GrantedAuthorityJson(PermissionEnum.FHIR_CAPABILITIES);
   GrantedAuthorityJson fhirClient = new GrantedAuthorityJson(PermissionEnum.ROLE_FHIR_CLIENT);

   // Get an Admin JSON client
   AdminJsonRestClient adminClient = harness.getAdminJsonClient();
   UserDetailsJson userDetailsJson = new UserDetailsJson();
   userDetailsJson.setUsername("observation-user");
   userDetailsJson.setPassword("HJ!cJm9qO!tYzy");
   userDetailsJson.setEmail("an-email@address.com");
   userDetailsJson.addAuthorities(fhirClient, metadataPermission, readAllObservationsPermission);

   //Ensure the user has no ID
   assertThat(userDetailsJson.getPid()).isNull();

   userDetailsJson = adminClient.userCreate("Master", "local_security", userDetailsJson);

   //Ensure the user has an ID now
   assertThat(userDetailsJson.getPid()).isNotNull();

   IGenericClient userFhirClient = harness.getFhirClient();
   userFhirClient.registerInterceptor(new BasicAuthInterceptor("observation-user", "HJ!cJm9qO!tYzy"));

   //Ensure they can query observations
   Bundle observations = userFhirClient.search().forResource("Observation").returnBundle(Bundle.class).execute();
   assertThat(observations.getEntry()).hasSize(0);

   //Ensure they can't query another arbitrary resource type.
   try {
      userFhirClient.search().forResource("DiagnosticReport").returnBundle(Bundle.class).execute();
   } catch (ForbiddenOperationException e) {
      assertThat(e).hasMessageContaining("HTTP 403 Forbidden: Access denied");
   }
}

This test:

  1. Gets an Admin JSON client 2. Creates a user with specific FHIR permissions (read Observations and access metadata) 3. Verifies the user was created successfully 4. Tests that the user can access Observation resources 5. Tests that the user cannot access unauthorized resource types (DiagnosticReport)

12.1.2HL7v2 Testing

 

Smile CDR supports HL7v2 messaging, and the SmileHarness provides an HL7V2RestClient that you can use to send HL7v2 messages in your tests. This section demonstrates how to use the HL7v2 client in your integration tests.

12.1.2.1Getting an HL7v2 Client

The first step is to get an HL7v2 client from the harness:

@Test
void testGetHL7V2Client() {
    // Get an HL7V2 client
    HL7V2RestClient hl7v2Client = harness.getHL7V2RestClient();
    
    // Verify the client was created successfully
    assertThat(hl7v2Client).isNotNull();
}

This test:

  1. Gets an HL7v2 client from the harness 2. Verifies the client was created successfully

12.1.2.2Sending an ADT Message

One common HL7v2 message type is the ADT (Admission, Discharge, Transfer) message. Here's how to create and send an ADT_A01 message:

@Test
void testSendADTMessage() throws HL7Exception {
    // Get an HL7V2 client
    HL7V2RestClient hl7v2Client = harness.getHL7V2RestClient();
    
    // Create a sample ADT_A01 (admission) message
    ADT_A01 adt = new ADT_A01();
    MSH msh = adt.getMSH();
    
    // Set required MSH fields
    msh.getFieldSeparator().setValue("|");
    msh.getEncodingCharacters().setValue("^~\\&");
    msh.getSendingApplication().getNamespaceID().setValue("TestApp");
    msh.getSendingFacility().getNamespaceID().setValue("TestFacility");
    msh.getReceivingApplication().getNamespaceID().setValue("SmileCDR");
    msh.getReceivingFacility().getNamespaceID().setValue("SmileCDR");
    msh.getDateTimeOfMessage().getTime().setValue("20230101120000");
    msh.getMessageType().getMessageCode().setValue("ADT");
    msh.getMessageType().getTriggerEvent().setValue("A01");
    msh.getMessageType().getMessageStructure().setValue("ADT_A01");
    msh.getMessageControlID().setValue("123456");
    msh.getProcessingID().getProcessingID().setValue("P");
    msh.getVersionID().getVersionID().setValue("2.5");
    
    // Set patient information
    adt.getPID().getPatientID().getIDNumber().setValue("12345");
    adt.getPID().getPatientName(0).getFamilyName().getSurname().setValue("Smith");
    adt.getPID().getPatientName(0).getGivenName().setValue("John");
    adt.getPID().getDateTimeOfBirth().getTime().setValue("19800101");
    adt.getPID().getAdministrativeSex().setValue("M");
    
    // Send the message to the server
    Message response = hl7v2Client.sendMessage(adt);
    
    // Verify we received a response
    assertThat(response).isNotNull();
    
    // Verify the response is an acknowledgment
    assertThat(response.getName()).contains("ACK");
}

This test:

  1. Gets an HL7v2 client 2. Creates a sample ADT_A01 (admission) message 3. Sets the required MSH (Message Header) fields 4. Sets patient information 5. Sends the message to the server 6. Verifies that a valid acknowledgment response is received

12.1.2.3Sending a Raw String Message

You can also send HL7v2 messages as raw strings:

@Test
void testSendStringMessage() throws HL7Exception {
    // Get an HL7V2 client
    HL7V2RestClient hl7v2Client = harness.getHL7V2RestClient();
    
    // Create a raw HL7v2 message as a string
    String rawMessage = "MSH|^~\\&|TestApp|TestFacility|SmileCDR|SmileCDR|20230101120000||ADT^A01^ADT_A01|123456|P|2.5\r" +
            "PID|||12345||Smith^John||19800101|M";
    
    // Send the message to the server
    Message response = hl7v2Client.sendMessage(rawMessage);
    
    // Verify we received a response
    assertThat(response).isNotNull();
    
    // Verify the response is an acknowledgment
    assertThat(response.getName()).contains("ACK");
}

This test:

  1. Gets an HL7v2 client 2. Creates a raw HL7v2 message as a string 3. Sends the message to the server 4. Verifies that a valid acknowledgment response is received

12.1.2.4Using the Test Data Helper

Smile CDR provides a test data helper class that can generate sample HL7v2 messages for you:

@Test
void testUsingTestDataHelper() throws HL7Exception {
    // Get an HL7V2 client
    HL7V2RestClient hl7v2Client = harness.getHL7V2RestClient();
    
    // Get the FHIR context from the harness
    FhirContext fhirContext = harness.getFhirContext();
    
    // Create a test data helper
    Hl7V2TestDataHelper testDataHelper = Hl7V2TestDataHelper.buildDefault(fhirContext);
    
    // Create a sample ADT_A01 message using the helper
    ADT_A01 adtA01 = testDataHelper.createAdtA01();
    
    // Send the message to the server
    Message response = hl7v2Client.sendMessage(adtA01);
    
    // Verify we received a response
    assertThat(response).isNotNull();
    
    // Verify the response is an acknowledgment
    assertThat(response.getName()).contains("ACK");
}

This test:

  1. Gets an HL7v2 client 2. Gets the FHIR context from the harness 3. Creates a test data helper 4. Creates a sample ADT_A01 message using the helper 5. Sends the message to the server 6. Verifies that a valid acknowledgment response is received

12.1.3Customizing SmileCdrContainer

 

Everything we've seen so far has been running smile CDR in its Out-of-box-experience mode. This means that:

  • It is using the default properties configuration file.
  • There is no seed data.
  • There are no implementation guides loaded.
  • There are no custom interceptors loaded.
  • There are no custom search parameres defined.
  • There is no troubleshooting logging enabled.

To support writing automated tests against a specific configuration, SmileCdrContainer allows you to customize all of the above to suit your own needs.

12.1.3.1Custom Properties Configuration File

The node configuration properties file controls the behaviour of Smile CDR. When booting a SmileCdrContainer, it will use the default cdr-config-master.properties that is shipped with Smile CDR. You may customize that by calling withPropertiesFile() on the container instance.

  • withPropertiesFile(String thePropertiesFilename) &ndash: If you invoke this method on the SmileCdrContainer during setup, it will use the file you have provided instead of the default properties file. This file must be on the classpath.

This allows you to customize Smile CDR to your specific test case. You can:

  • Add modules
  • Remove modules
  • Change settings in modules
  • change the node ID

Let's have a look at a simple example, CustomConfigurationIT.java.

@Container
private static final SmileCdrContainer container = new SmileCdrContainer()
   .withPropertiesFile("config-with-two-persistence.properties");

private static SmileHarness harness;

@BeforeAll
public static void setup() {
   harness = container.getHarness();
}

This test demonstrates how to use a custom properties file with SmileCdrContainer. The key part is using the withPropertiesFile() method to specify your custom configuration file. Have a look at the config-with-two-persistence.properties to see what this looks like. This new custom configuration file does two specific things:

  1. Creates a new persistence module called persistence_two, which is also an R4 Persistence module.
  2. Creates a new FHIR Endpoint module called fhir_endpoint_two which is hosted on port 8888
@Test
void testCreatePatientsOnTwoSeparateRepositories() {
   // Get a FHIR client with admin credentials for fhir_endpoint
   IGenericClient fhirClientFirstModule = harness.getSuperuserFhirClient(8000);

   // Get a FHIR client with admin credentials for fhir_endpoint_two
   IGenericClient fhirClientSecondModule = harness.getSuperuserFhirClient(8888);

   // Create a new patient
   Patient patient = new Patient();
   patient.addName().setFamily("Johnson").addGiven("Robert");
   patient.setGender(Enumerations.AdministrativeGender.MALE);

   // Save the patient to the first endpoint
   patient = (Patient) fhirClientFirstModule.create().resource(patient).execute().getResource();

   // Retrieve the patient by ID
   Patient retrievedPatient = fhirClientFirstModule.read().resource(Patient.class).withId(patient.getId()).execute();
   // Verify the retrieved patient matches what we created
   assertThat(retrievedPatient.getName().get(0).getFamily()).isEqualTo("Johnson");
   assertThat(retrievedPatient.getName().get(0).getGiven().get(0).toString()).isEqualTo("Robert");

   //Ensure that the second client doesn't know about it(since its in a different repository!)
   try {
      fhirClientSecondModule.read().resource(Patient.class).withId(patient.getId()).execute();
   } catch (ResourceNotFoundException e) {
      assertThat(e).hasMessageContaining("HTTP 404 Not Found");
   }
}

12.1.3.2Modifying bootstrapping information

You must take care when providing a custom properties file. SmileCdrContainer and SmileHarness expect the default credentials to work, and also expect the JSON Admin API to be running on its default port (9000).

If you modify your configuration in such a way that any of the above are no longer true, you'll notice that the Smile Harness cannot connect to the running container. This can be remedied by providing a custom HarnessContext object.

Have a look at config-with-alternate-admin-json.properties in your project's resources directory. This file should contain the custom configuration for your Smile CDR instance. The specific change we care about here is that we are modifying the Admin JSON module's port value:

Default value:

module.admin_json.config.port                                                              =9000

Modified value:

module.admin_json.config.port                                                              =9123

In this example, we have modified our configuration properties file to have the Admin Json module run on port 9123, so we will have to provide a custom HarnessContext to the container.

// In this example, we're using a custom properties file that configures
// the Admin JSON module to run on port 9123 instead of the default 9000
@Container
private static SmileCdrContainer container = new SmileCdrContainer()
    .withPropertiesFile("config-with-alternate-admin-json.properties")
    .withCustomHarnessContext(new HarnessContext(
        "http",
     "localhost",  // baseUrl
        9123,                // jsonAdminPort - matches our custom config
        "admin",             // username
        "password"           // password
    ));

private static SmileHarness harness;

@BeforeAll
public static void setup() {
    harness = container.getHarness();
}

This test demonstrates how to use a custom HarnessContext with SmileCdrContainer. The key part is using the withCustomHarnessContext() method to specify a custom HarnessContext that matches your modified configuration:

@Test
void testGetAdminControllerWithCustomHarnessContext() {
    // Get a FHIR client with admin credentials
  AdminJsonRestClient adminJsonClient = harness.getAdminJsonClient(9123);

  // Ensure we got the client
  assertThat(adminJsonClient).isNotNull();

  // Ensure that the bootstrapping worked, and we can retrieve other clients
  IGenericClient fhirClient = harness.getSuperuserFhirClient();
  IFetchConformanceUntyped capabilities = fhirClient.capabilities();
  assertThat(capabilities).isNotNull();
}

12.1.3.3Loading custom interceptors

12.1.4Conclusion

 

In this tutorial, you've learned how to:

  1. Set up a basic test with SmileCdrContainer and SmileHarness
  2. Interact with the FHIR API 3. Work with the Admin API 4. Send and process HL7v2 messages 5. Use a custom configuration

These examples should give you a good starting point for writing your own tests. Remember that SmileCdrContainer and SmileHarness are designed to make testing with Smile CDR easier, so take advantage of their features to write comprehensive tests for your applications.

For more information on the clients available through SmileHarness, see Available Clients.

For recommendations on effective testing with SmileCdrContainer and SmileHarness, see Best Practices.