Unit Testing Bots
Unit testing your Bot code is crucial to ensuring accurate data and workflows. This guide will go over the most common unit testing patterns.
Medplum provides the MockClient
class to help unit test Bots on your local machine. You can also see a reference implementation of simple bots with tests in our Medplum Demo Bots repo.
Set up your test framework
The first step is to set up your test framework in your Bots package. Medplum Bots will should work with any typescript/javascript test runner, and the Medplum team has tested our bots with jest and vitest. Follow the instructions for your favorite framework to set up you package.
Next you'll want to index the FHIR schema definitions. To keep the client small, the MockClient
class only ships with a subset of the FHIR schema. Index the full schema as shown below, either in a beforeAll
function or setup file, to make sure your test queries work.
beforeAll(() => {
indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle);
indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle);
indexSearchParameterBundle(readJson('fhir/r4/search-parameters.json') as Bundle<SearchParameter>);
});
Our Medplum Demo Bots repo also contains recommended eslintrc, tsconfig, and vite.config settings for a faster developer feedback cycle.
Write your test file
After setting up your framework, you're ready to write your first test file! The most common convention is to create a single test file per bot, named <botname>.test.ts
.
You will need to import your bot's handler
function, in addition to the other imports required by your test framework. You'll call this handler
from each one of your tests.
import { handler } from './my-bot';
Write your unit test
Most bot unit tests follow a common pattern:
- Create a Medplum
MockClient
- Create mock resources
- Invoke the handler function
- Query mock resources and assert test your test criteria
The finalize-report tests are a great example of this pattern.
Create a MockClient
The Medplum MockClient
class extends the MedplumClient
class, but stores resources in local memory rather than persisting them to the server. This presents a type-compatible interface to the Bot's handler function, which makes it ideal for unit tests.
const medplum = new MockClient();
We recommend creating a MockClient
at the beginning of each test, to avoid any cross-talk between tests.
The MockClient does not yet perfectly replicate the functionality of the MedplumClient
class, as this would require duplicating the entire server codebase. Some advanced functionality does not yet behave the same between MockClient
and MedplumClient
, including:
medplum.graphql
medplum.executeBatch
- FHIR $ operations
The Medplum team is working on bringing these features to parity as soon as possible. You can join our Github discussion here
Create mock resource resources
Most tests require setting up some resources in the mock environment before running the Bot. You can use createResource()
and updateResource()
to add resources to your mock server, just as you would with a regular MedplumClient
instance.
The finalize-report bot from Medplum Demo Bots provides a good example. Each test sets up a Patient, an Observation, and a DiagnosticReport before invoking the bot.
Example: Create Resources
// Create the Patient
const patient: Patient = await medplum.createResource({
resourceType: 'Patient',
name: [
{
family: 'Smith',
given: ['John'],
},
],
});
// Create an observation
const observation: Observation = await medplum.createResource({
resourceType: 'Observation',
status: 'preliminary',
subject: createReference(patient),
code: {
coding: [
{
system: LOINC,
code: '39156-5',
display: 'Body Mass Index',
},
],
text: 'Body Mass Index',
},
valueQuantity: {
value: 24.5,
unit: 'kg/m2',
system: UCUM,
code: 'kg/m2',
},
});
// Create the Report
const report: DiagnosticReport = await medplum.createResource({
resourceType: 'DiagnosticReport',
status: 'preliminary',
result: [createReference(observation)],
});
Invoke your Bot
After setting up your mock resources, you can invoke your bot by calling your bot's handler function. See the "Bot Basics" tutorial for more information about the arguments to handler
// Invoke the Bot
const contentType = 'application/fhir+json';
await handler(medplum, { input: report, contentType, secrets: {} });
Query the results
Most of the time, Bots will create or modify resources on the Medplum server. To test your bot, you can use your MockClient
to query the state of resources on the server, just as you would with a MedplumClient
in production.
To check the bot's response, simply check the return value of your handler
function.
The after running the Bot, the finalize-report bot's tests read the updated DiagnosticReport
and Observation
resources to confirm their status.
Example: Query the results
// Check the output by reading from the 'server'
// We re-read the report from the 'server' because it may have been modified by the Bot
const checkReport = await medplum.readResource('DiagnosticReport', report.id as string);
expect(checkReport.status).toBe('final');
// Read all the Observations referenced by the modified report
if (checkReport.result) {
for (const observationRef of checkReport.result) {
const checkObservation = await medplum.readReference(observationRef);
expect(checkObservation.status).toBe('final');
}
}
Many times, you'd like to make sure your Bot is idempotent. This can be accomplished by calling your bot twice, and using your test framework's spyOn
functions to ensure that no resources are created/updated in the second call.
Example: Idempotency test
// Invoke the Bot for the first time
const contentType = 'application/fhir+json';
await handler(medplum, { input: report, contentType, secrets: {} });
// Read back the report
const updatedReport = await medplum.readResource('DiagnosticReport', report.id as string);
// Create "spys" to catch calls that modify resources
const updateResourceSpy = jest.spyOn(medplum, 'updateResource');
const createResourceSpy = jest.spyOn(medplum, 'createResource');
const patchResourceSpy = jest.spyOn(medplum, 'patchResource');
// Invoke the bot a second time
await handler(medplum, { input: updatedReport, contentType, secrets: {} });
// Ensure that no modification methods were called
expect(updateResourceSpy).not.toBeCalled();
expect(createResourceSpy).not.toBeCalled();
expect(patchResourceSpy).not.toBeCalled();