Skip to main content

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:

  1. Create a Medplum MockClient
  2. Create mock resources
  3. Invoke the handler function
  4. 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.

Caution

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: 'http://loinc.org',
code: '39156-5',
display: 'Body Mass Index',
},
],
text: 'Body Mass Index',
},
valueQuantity: {
value: 24.5,
unit: 'kg/m2',
system: 'http://unitsofmeasure.org',
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');
}
}
A note on idempotency

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();