On this page

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`](https://github.com/medplum/medplum/blob/main/packages/mock/src/client.ts#L68) 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](https://github.com/medplum/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 should work with any TypeScript/JavaScript test runner, and the Medplum team has tested our bots with [Jest](https://jestjs.io/) and [Vitest](https://vitest.dev/). Follow the instructions for your favorite framework to set up your 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](https://vitest.dev/api/#beforeall) or [setup file](https://vitest.dev/config/#setupfiles), to make sure your [test queries](/content/docs/bots/unit-testing-bots#query-the-results/index.html) work.

```ts
import { SEARCH_PARAMETER_BUNDLE_FILES, readJson } from '@medplum/definitions';

beforeAll(() => {

indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle);

indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle);

for (const filename of SEARCH_PARAMETER_BUNDLE_FILES) {

indexSearchParameterBundle(readJson(filename) as Bundle<SearchParameter>);

}

});
```

Our [Medplum Demo Bots](https://github.com/medplum/medplum-demo-bots) repo also contains recommended [eslintrc](https://github.com/medplum/medplum-demo-bots/blob/main/.eslintrc.json), [tsconfig](https://github.com/medplum/medplum-demo-bots/blob/main/tsconfig.json), and [vite.config](https://github.com/medplum/medplum-demo-bots/blob/main/vite.config.ts) 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.

```typescript
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](https://github.com/medplum/medplum-demo-bots/blob/main/src/finalize-report.ts) 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.

```ts
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 all functionality of the `MedplumClient` class. Here are the known differences:

- Chained search
- Search `_include` and `_revinclude` parameters
- Full compliance for token search parameters
- More accurate schema validation
- **Authentication**
- **Most FHIR $ operations**
- **`$validate`**
- **`Bot/$execute`**
- **Terminology service** (e.g., `ValueSet/$expand`)
- **FHIRcast**
- **Websocket subscriptions**
- **Project admin calls** (new project, invite user, create client application, etc.)

The Medplum team is working on bringing these features to parity as soon as possible. For related discussion, see [related GitHub issue](https://github.com/medplum/medplum/issues/6889).

### Create test data  
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](https://github.com/medplum/medplum-demo-bots/blob/main/src/finalize-reports.test.ts) from [Medplum Demo Bots](https://github.com/medplum/medplum-demo-bots/) provides a good example. Each test sets up a [Patient](/content/docs/api/fhir/resources/patient/index.html), an [Observation](/content/docs/api/fhir/resources/observation/index.html), and a [DiagnosticReport](/content/docs/api/fhir/resources/diagnosticreport/index.html) before invoking the bot.

Example: Create Resources

```ts
//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',

code: { text: 'Body Mass Index' },

result: [createReference(observation)],

});
```

#### Creating Rich Test Data in Batches  
Creating individual test resources can be time consuming and tedious, so the `MockClient` also offers the ability to use batch requests to set up sample data. See the [FHIR Batch Requests guide](/content/docs/fhir-datastore/fhir-batch-requests/index.html) for more details on batch requests.

The `MockClient` offers the `executeBatch` helper function to easily execute batch requests and works in the same way that the standard Medplum Client does.

The below example is from the [find matching patients bot tests](https://github.com/medplum/medplum/blob/main/examples/medplum-demo-bots/src/deduplication/find-matching-patients.test.ts). Additionally, you can view the [patient data here](https://github.com/medplum/medplum/blob/main/examples/medplum-demo-bots/src/deduplication/patient-data.json).

Example: Creating a large set of patient data with a batch request

```ts
// import a Bundle of test data from 'patient-data.json'
import patientData from './patient-data.json';

// Load the sample data from patient-data.json

beforeEach<TestContext>(async (context) => {

context.medplum = new MockClient();

await context.medplum.executeBatch(patientData as Bundle);

});

test<TestContext>('Created RiskAssessment', async ({ medplum }) => {

// Read the patient. The `medplum` mock client has already been pre-populated with test data in `beforeEach`

const patients = await medplum.searchResources('Patient', { given: 'Alex' });

await handler(medplum, {

bot: { reference: 'Bot/123' },

input: patients?.[0],

contentType: ContentType.FHIR_JSON,

secrets: {},

});

// We expect two risk assessments to be created for the two candidate matches

const riskAssessments = await medplum.searchResources('RiskAssessment');

expect(riskAssessments.length).toBe(2);

expect(riskAssessments.every((assessment) => resolveId(assessment.subject) === patients[0].id));

});
```

In this example we create the `MockClient`, then the test data by calling `executeBatch` in the `beforeEach` function. The `beforeEach` function is an optimization that will run before each test, so you do not need to create the data as a part of every test.

Once you have created your data, you can write your tests. The above example uses test contexts, a feature of the [Vitest test framework](https://vitest.dev/guide/test-context.html#test-context). It allows you to pass in the MockClient `medplum` as part of your test context. This test is checking that a `RiskAssessment` was created when looking for potential duplicate patients.

#### Using the Medplum CLI  
If you have a dev project that already has rich data, you can use the Medplum CLI to easily convert this data into test data.

The Medplum CLI offers the optional `--as-transaction` flag when using the `medplum get` command. A `GET` request returns a `Bundle` with `type=searchset`, but this flag will convert it to `type=transaction`.

Example: Get a patient and all encounters that reference them as a transaction

```ts
medplum get --as-transaction 'Patient?name=Alex&_revinclude=Encounter:patient'
```

This example searches for all `Patient` resources named 'Alex'. It also uses the [`_revinclude` parameter](/content/docs/search/includes/index.html) to search for all `Encounter` resources that reference those patients.

A transaction `Bundle` can be used directly in a batch request, and can be passed as an argument to `executeBatch` on your `MockClient`. This allows you to easily create test resources from already existing data.

### 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](/content/docs/bots/bot-basics#editing-a-bot/index.html) for more information about the arguments to `handler`

```ts
// Invoke the Bot

const contentType = 'application/fhir+json';

await handler(medplum, {

bot: { reference: 'Bot/123' },

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](https://github.com/medplum/medplum-demo-bots/blob/main/src/finalize-reports.test.ts) read the updated `DiagnosticReport` and `Observation` resources to confirm their status.

Example: Query the results

```ts
// 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

for (const observationRef of checkReport.result ?? EMPTY) {

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](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning). 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

```ts
// Invoke the Bot for the first time

const contentType = 'application/fhir+json';

await handler(medplum, {

bot: { reference: 'Bot/123' },

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 = vi.spyOn(medplum, 'updateResource');

const createResourceSpy = vi.spyOn(medplum, 'createResource');

const patchResourceSpy = vi.spyOn(medplum, 'patchResource');

// Invoke the bot a second time

await handler(medplum, {

bot: { reference: 'Bot/123' },

input: updatedReport,

contentType,

secrets: {},

});

// Ensure that no modification methods were called

expect(updateResourceSpy).not.toHaveBeenCalled();

expect(createResourceSpy).not.toHaveBeenCalled();

expect(patchResourceSpy).not.toHaveBeenCalled();
```

### Using the Medplum CLI  
If you have a dev project that already has rich data, you can use the Medplum CLI to easily convert this data into test data.

Example: Get a patient and all encounters that reference them as a transaction

```cli
// medplum get --as-transaction 'Patient?name=Alex&_revinclude=Encounter:patient'
```
