4. Test the Patient Provider
On the previous page, you implemented the Patient read and search, and removed the unit tests that expected HTTP 501. On this page, you add tests for the new code: unit tests for the mapper and the provider, and integration tests that call your deployed service. The integration tests that came with the archetype still expect HTTP 501, so you update them too.
How We Test
Our testing philosophy combines unit and integration testing to measure the coverage we have around our endpoints. Considered together like this, test coverage becomes a meaningful metric of the state of our services. We use real services as much as possible, and mocks only when necessary, usually only to stand in for external systems, as WireMock does for the Mobile MVI Service here. Integration tests come first; unit tests are for logic that is easier to test in isolation, such as the mapper, or for cases that are hard to reach from an integration test.
JUnit, Mockito, and AssertJ
The tests in this guide use three libraries. If you have used them before, skip ahead to Test the Mapper.
JUnit 5 runs the tests:
-
@Testmarks a test method. A test passes when it finishes without an exception. -
@BeforeEachmarks a method that runs before every test, for example to create the object under test. -
@ParameterizedTestruns a test once for each row of data, for example from a@CsvSource({"M, MALE", "F, FEMALE"}). Each row counts as one test.
AssertJ checks the results. Every assertion starts with assertThat(actual), followed by what you expect:
| Assertion | Passes when |
|---|---|
|
|
|
The list has two elements, or none |
|
The list has exactly one element, and the nested assertions pass for it |
|
The text contains, or doesn’t contain, the given text |
|
|
Mockito stands in for the objects your class depends on, so a unit test doesn’t need the real Mobile MVI Service:
-
With
@ExtendWith(MockitoExtension.class)on the test class, a field annotated with@Mockgets a mock: an object whose methods returnnull, an emptyOptional, or0, until you tell them otherwise. -
when(mock.method(args)).thenReturn(value)tells the mock what to return. This is called stubbing. -
eq(value)andany()match arguments. If you use one for an argument, use them for all the arguments of that call. -
The tests use strict stubs: a test fails if it stubs a call that never happens. This catches tests that don’t test what they claim to.
A Worked Example
Here is the first read test of PatientProviderTest, which you add below. It follows the pattern of every unit test in this guide, arrange, act, assert:
@Test
void read_staff() {
// Arrange: a staff user, and an MVI that knows the patient
givenUser(JwtTestUtils.V2.createStaffUser());
when(mviClient.getPerson(eq(OTHER_ICN), any())).thenReturn(Optional.of(new Person().lastName("THIRTYTWO")));
// Act: read the patient
final Patient patient = provider.read(requestDetails, new IdType(OTHER_ICN));
// Assert: the Patient has the ICN as its id, and the name from MVI
assertThat(patient.getIdPart()).isEqualTo(OTHER_ICN);
assertThat(patient.getNameFirstRep().getFamily()).isEqualTo("THIRTYTWO");
}
-
givenUseris a helper method in the test class. It puts the JWT user thatPatientAccessreads into the mocked request. -
getPersontakes the ICN and the caller’s JWT. The test doesn’t care which JWT is passed on, so it matches the JWT withany(), and therefore the ICN witheq(…). -
The test checks what matters about the result, not every field: the mapper has its own tests.
For a test that expects an exception, create everything the call needs before the lambda, so that only the call that should throw is inside it:
final IdType id = new IdType("not-an-icn");
assertThatThrownBy(() -> provider.read(requestDetails, id))
.isInstanceOf(InvalidRequestException.class);
Test the Mapper
Create the test package mapper (in src/test/java/gov/va/mobile/fhirstarter/v1) and a test class named PatientMapperTest. The first two tests are complete; implement the other two:
package gov.va.mobile.fhirstarter.v1.mapper;
import ca.uhn.fhir.context.FhirContext;
import gov.va.mobile.mvi.v1.model.Address;
import gov.va.mobile.mvi.v1.model.Person;
import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import static gov.va.mobile.security.FhirSystems.ICN_SYSTEM;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link PatientMapper}.
*/
class PatientMapperTest {
/**
* ICN of PATIENT FIFTEEN.
*/
private static final String ICN = "10115V737384";
/**
* The mapper under test.
*/
private final PatientMapper mapper = new PatientMapper();
/**
* Verify every MVI field the service uses is mapped to the Patient.
*/
@Test
void toPatient() {
final Patient patient = mapper.toPatient(ICN, fifteen());
assertThat(patient.getIdPart()).isEqualTo(ICN);
assertThat(patient.getIdentifierFirstRep().getSystem()).isEqualTo(ICN_SYSTEM);
assertThat(patient.getIdentifierFirstRep().getValue()).isEqualTo(ICN);
final HumanName name = patient.getNameFirstRep();
assertThat(name.getUse()).isEqualTo(HumanName.NameUse.OFFICIAL);
assertThat(name.getFamily()).isEqualTo("FIFTEEN");
assertThat(name.getGivenAsSingleString()).isEqualTo("PATIENT");
assertThat(patient.getGender()).isEqualTo(AdministrativeGender.MALE);
assertThat(patient.getBirthDateElement().getValueAsString()).isEqualTo("1953-04-01");
assertThat(patient.getTelecomFirstRep().getSystem()).isEqualTo(ContactPoint.ContactPointSystem.PHONE);
assertThat(patient.getTelecomFirstRep().getValue()).isEqualTo("(123)234-5678");
final org.hl7.fhir.r4.model.Address address = patient.getAddressFirstRep();
assertThat(address.getLine().getFirst().getValue()).isEqualTo("1111 Any Street");
assertThat(address.getCity()).isEqualTo("Any Town");
assertThat(address.getState()).isEqualTo("WV");
assertThat(address.getPostalCode()).isEqualTo("99998-3414");
assertThat(address.getCountry()).isEqualTo("USA");
}
/**
* Verify the SSN that MVI returns doesn't appear anywhere in the Patient.
*/
@Test
void toPatient_doesNotMapSsn() {
final Person person = fifteen().ssn("666000015");
final Patient patient = mapper.toPatient(ICN, person);
final String json = FhirContext.forR4Cached().newJsonParser().encodeResourceToString(patient);
assertThat(json).doesNotContain("666000015");
}
/**
* Verify the MVI gender codes are mapped to FHIR genders: M, F, another code, and a missing code.
*/
@Test
void toGender() {
// TODO: implement
}
/**
* Verify an invalid MVI birth time results in no birth date.
*/
@Test
void toBirthDate_invalid() {
// TODO: implement
}
/**
* Create the MVI demographics of PATIENT FIFTEEN, as returned by the WireMock stub.
*
* @return the demographics
*/
private static Person fifteen() {
return new Person()
.lastName("FIFTEEN")
.firstName("PATIENT")
.birthTime("19530401")
.gender("M")
.telecom("(123)234-5678")
.address(new Address()
.street("1111 Any Street")
.city("Any Town")
.state("WV")
.zipCode("99998-3414")
.country("USA"));
}
}
-
toPatient_doesNotMapSsnencodes the whole Patient as JSON, so it catches the SSN wherever a future change might put it. -
For
toGender, consider a@ParameterizedTestwith a@CsvSource, for example"M, MALE", and callPatientMapper.toGender(code)directly. Cover five codes:M,F, another code such asX(UNKNOWN), a blank code (""), and a missing code (null). The last two both map tonull. In a@CsvSource,"'', "passes a blank code and","passesnullfor both columns. -
For
toBirthDate_invalid, callPatientMapper.toBirthDatewith a birth time that has the right format but an impossible date, such as19531345.
Update the Provider Unit Tests
On page 3, you gave PatientProviderTest a mocked MviClient, and removed the tests that expected HTTP 501. Now add tests for the read and search. Make these changes to PatientProviderTest.java:
-
Add the following search tests after the
getResourceTypetest:@Test void search_staff() { givenUser(JwtTestUtils.V2.createStaffUser()); when(mviClient.getPerson(eq(OTHER_ICN), any())).thenReturn(Optional.of(new Person().lastName("THIRTYTWO"))); final List<Patient> patients = provider.search(requestDetails, new TokenParam(ICN_SYSTEM, OTHER_ICN)); assertThat(patients).singleElement() .satisfies(patient -> assertThat(patient.getIdPart()).isEqualTo(OTHER_ICN)); } @Test void search_veteranSelfWithoutSystem() { givenUser(JwtTestUtils.V2.createVeteranUser(VETERAN_ICN)); when(mviClient.getPerson(eq(VETERAN_ICN), any())).thenReturn(Optional.of(new Person().lastName("FIFTEEN"))); final List<Patient> patients = provider.search(requestDetails, new TokenParam(VETERAN_ICN)); assertThat(patients).hasSize(1); } @Test void search_notFound() { givenUser(JwtTestUtils.V2.createStaffUser()); when(mviClient.getPerson(eq(OTHER_ICN), any())).thenReturn(Optional.empty()); final List<Patient> patients = provider.search(requestDetails, new TokenParam(ICN_SYSTEM, OTHER_ICN)); assertThat(patients).isEmpty(); } -
Add the following read tests just before the
givenUsermethod.read_staffis the worked example from JUnit, Mockito, and AssertJ; implement the other two read tests:/** * A staff user reads a patient that MVI knows. */ @Test void read_staff() { givenUser(JwtTestUtils.V2.createStaffUser()); when(mviClient.getPerson(eq(OTHER_ICN), any())).thenReturn(Optional.of(new Person().lastName("THIRTYTWO"))); final Patient patient = provider.read(requestDetails, new IdType(OTHER_ICN)); assertThat(patient.getIdPart()).isEqualTo(OTHER_ICN); assertThat(patient.getNameFirstRep().getFamily()).isEqualTo("THIRTYTWO"); } /** * A staff user reads a patient that MVI has no record of: ResourceNotFoundException. */ @Test void read_notFound() { // TODO: implement } /** * A veteran reads the record of another patient: ForbiddenOperationException. */ @Test void read_veteranOther() { // TODO: implement } @Test void read_invalidIcn() { final IdType id = new IdType("not-an-icn"); assertThatThrownBy(() -> provider.read(requestDetails, id)) .isInstanceOf(InvalidRequestException.class); } -
Add these imports:
import gov.va.mobile.mvi.v1.model.Person; import java.util.List; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq;
The other tests stay as they are: the access rules haven’t changed.
Stuck, or want to check your work? Compare your file with fhir-starter-service/src/test/java/gov/va/mobile/fhirstarter/v1/resource/PatientProviderTest.java in the "FHIR starter page 4" commit of the reference branch; see When You Get Stuck.
|
-
Follow
read_staff. Pass only the ICN toIdType: the build’s PMD check fails a class that repeats the same string literal four times or more, such as"Patient". -
read_notFoundexpects aResourceNotFoundException. Addimport ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;when you use it. -
Mockito’s strict stubs fail a test that sets up a
when(…)it never uses. Inread_veteranOther, the provider denies access before it calls MVI, so don’t stubmviClientthere.
Update the Integration Tests
The generated PatientResourceITCase expected HTTP 501. Change it to test against your WireMock stubs:
-
Replace the class Javadoc with the following:
/** * Integration tests for the Patient resource provider, covering the role based access rules of the service. * * <p>The patient data comes from the WireMock stubs of the Mobile MVI Service in * {@code kubernetes/components/dev/wiremock-data/mvi}. */ -
Add a constant for the patient that MVI has no record of, after
OTHER_ICN:/** * ICN of a patient that MVI has no record of. */ private static final String UNKNOWN_ICN = "1012845331V153043"; -
Replace the
searchByIcn_staffandsearchByIcn_veteranSelftests with the following. The third test,searchByIcn_notFound, is new:/** * Verify a staff user may search for any patient. */ @Test void searchByIcn_staff() { final IGenericClient client = newClient(serviceUrl, JwtTestUtils.V2.createStaffJwt()); final Bundle bundle = searchByIcn(client, OTHER_ICN).execute(); assertThat(bundle.getEntry()).singleElement() .satisfies(entry -> assertThat(((Patient) entry.getResource()).getNameFirstRep().getFamily()) .isEqualTo("THIRTYTWO")); } /** * Verify a veteran user may search for their own records. */ @Test void searchByIcn_veteranSelf() { final IGenericClient client = newClient(serviceUrl, JwtTestUtils.V2.createVeteranJwt(VETERAN_ICN)); final Bundle bundle = searchByIcn(client, VETERAN_ICN).execute(); assertThat(bundle.getEntry()).hasSize(1); } /** * Verify a search for a patient that MVI has no record of returns an empty Bundle. */ @Test void searchByIcn_notFound() { final IGenericClient client = newClient(serviceUrl, JwtTestUtils.V2.createStaffJwt()); final Bundle bundle = searchByIcn(client, UNKNOWN_ICN).execute(); assertThat(bundle.getEntry()).isEmpty(); } -
Replace the
readById_stafftest with the following, which also adds areadByIdhelper method, and implement the four TODO tests:/** * Verify a staff user may read any patient. */ @Test void readById_staff() { final IGenericClient client = newClient(serviceUrl, JwtTestUtils.V2.createStaffJwt()); final Patient patient = readById(client, OTHER_ICN).execute(); assertThat(patient.getIdPart()).isEqualTo(OTHER_ICN); assertThat(patient.getNameFirstRep().getFamily()).isEqualTo("THIRTYTWO"); } /** * Verify a veteran user may read their own record. */ @Test void readById_veteranSelf() { // TODO: implement } /** * Verify a veteran user may not read the record of another patient. */ @Test void readById_veteranOther() { // TODO: implement } /** * Verify reading a patient that MVI has no record of results in HTTP 404. */ @Test void readById_notFound() { // TODO: implement } /** * Verify a logical id that isn't a valid ICN is rejected. */ @Test void readById_invalidIcn() { // TODO: implement } /** * Build a Patient read ({@code GET [base]/Patient/<icn>}). * * @param client the FHIR client to read with * @param icn the ICN of the patient, which is the logical id of the Patient * @return the read, ready to execute */ private static IReadExecutable<Patient> readById(final IGenericClient client, final String icn) { return client.read().resource(Patient.class).withId(icn); } -
Remove the import of
NotImplementedOperationException, which is no longer used, and addimport static org.assertj.core.api.Assertions.assertThat;with the other static imports.
The searchByIcn_veteranOther, searchByIcn_invalidIcn, and searchByIcn_unauthenticated tests stay as they are.
Stuck, or want to check your work? Compare your file with fhir-starter-service/src/test/java/gov/va/mobile/fhirstarter/v1/PatientResourceITCase.java in the "FHIR starter page 4" commit of the reference branch; see When You Get Stuck.
|
-
The integration tests use the HAPI FHIR client (
IGenericClient), created bynewClientinBaseITCasewith a JWT fromJwtTestUtils. The client throws the HAPI exception that matches the HTTP status of an error response, for exampleForbiddenOperationExceptionfor HTTP 403. -
JwtTestUtils.V2.createVeteranJwt(icn)puts the ICN in the subject of the JWT, as jwtgen does, so it can read that patient’s records and no others. -
For
readById_notFound, usereadById(client, UNKNOWN_ICN)and expect aResourceNotFoundExceptionwhose message containsNo patient was found. Add its import when you use it. Compare it withsearchByIcn_notFound: searching for a patient that doesn’t exist isn’t an error, but reading one is. -
For
readById_invalidIcn, read"not-an-icn"and expect anInvalidRequestException.
Check the MVI Health Indicator
In HealthCheckITCase, check that the mvi health indicator is up, before the overall status:
assertActuatorHealth()
.withBaseUrl(actuatorUrl)
.hasComponentsUp("mvi")
.isUp();
Run the Build
The build measures the code coverage of the unit tests and the integration tests together, and fails if line or branch coverage drops below 80% (the thresholds are in pom.xml). After a full build, open target/site/jacoco-aggregate/index.html in a browser to see the coverage details.
Run the full build to make sure all tests and code quality checks pass:
./mvnw clean install -Pwith-skaffold
|
For a quicker check while you work, use |
If you have test failures and need to debug, refer to the debug guide: start the service with skaffold debug, and run the integration tests from your IDE with breakpoints in your code.
Commit Your Work
The build passes again, so push your work from this page and the previous one:
git add .
git commit -m "Test the Patient read and search"
git push
You’re Done When
-
PatientMapperTest,PatientProviderTest, andPatientResourceITCasehave noTODOtests left -
All tests pass with
./mvnw clean install -Pwith-skaffold, and coverage is at least 80% -
Your work is committed and pushed
Continue to 5. Add FHIR Exception Handling to handle failures of the Mobile MVI Service.