Skip to content

TestScript Shorthand (TSH)

TestScript Shorthand (TSH) is a concise notation for authoring FHIR TestScript resources. A typical TSH file is 60–70% shorter than the equivalent JSON/XML TestScript.

This page covers the TSH grammar as implemented in the FHIR Frog engine, verified runnable patterns, and common authoring traps.

Is TSH right for me?

Use TSH when: - You are testing a server's FHIR API with read, create, search, and delete operations - Your assertions check HTTP response codes, resource types, and FHIRPath expressions - You want concise, readable test definitions

Use JSON TestScript instead when: - You need system-level operations (e.g. $expand, $validate, $lookup) - You require search parameters in the operation URL (e.g. ?family=Smith) - You are read-testing with a specific resource id (e.g. read Patient/123) - You need request/response message templates

Document structure

Every TSH file begins with exactly one TestScript: declaration followed by metadata and test phases:

TestScript: HelloTest
Title: "Hello Test"
Description: "A simple example"
Status: #draft
Url: "http://example.org/TestScript/hello-test"

Fixture: p
  * resource = "patient.json"
  * autocreate = false
  * autodelete = false

Setup:
  * operation create Patient from "p" as created
  * assert responseCode = 201

Test: "Search patients"
  * operation search Patient as searchResp
  * assert responseCode = 200
  * assert resource = Bundle

Metadata

Field Required Purpose
TestScript: <id> Yes Resource identifier (becomes the id field in compiled JSON)
Title: "..." No Human-readable title
Description: "..." No Narrative description
Status: #<code> Yes Publication status: #draft, #active, #retired. Use #draft for authoring.
Url: "..." No Canonical URL; frog_tsh_compile adds a default if omitted

Fixtures

Fixtures are test data files (typically JSON or XML resources) referenced by operations. Declare a fixture once, and reference it by name in create/update operations.

Fixture: p
  * resource = "patient.json"
  * autocreate = false
  * autodelete = false

Fixture: o
  * resource = "observation.json"
  * autocreate = false
  * autodelete = false
  • resource = "filename": path relative to the TestScript (or absolute within the workspace); the file must exist when the script runs.
  • autocreate: not used by the FHIR Frog runner; set to false.
  • autodelete: if true, the fixture resource is deleted after the test. Deletion uses the resource's id field, so if the server assigned a different id, the resource leaks.

Known limit: fixture ids

The fixture id in Fixture: p is not the same as the resource's id in the file. When you use from "p", you are referencing the fixture name, not the resource id. The resource id (in the JSON "id" field) is extracted at runtime.

Test phases

A TSH file can contain up to three test phases:

Setup

Optional phase that runs before all test actions. Use it to create shared preconditions:

Setup:
  * operation create Patient from "p" as shared_patient
  * assert responseCode = 201

Test

Required; one or more named test blocks. Each Test is an independent sequence of actions:

Test: "Test name 1"
  * operation search Patient as resp1
  * assert responseCode = 200

Test: "Test name 2"
  * operation search Patient as resp2
  * assert responseCode = 200

Teardown

Optional phase that runs after all test actions. Teardown may contain operations only; assertions are dropped during compilation with a warning.

Teardown:
  * operation search Patient as cleanup_search

Operations

An operation represents a FHIR interaction.

* operation <type> <resource>[/<id>] [from "<fixture>"] [as <responseId>]

Operation types

Type Syntax Compiled to Notes
create create <Resource> from "<fixture>" HTTP POST Creates a new resource from a fixture file
search search <Resource> HTTP GET Searches for resources of a type
read read <Resource>/<id> HTTP GET Does not run; see below
update update <Resource>/<id> from "<fixture>" HTTP PUT See known limits
delete delete <Resource>/<id> HTTP DELETE Does not run; see below

Verified runnable patterns

These patterns have been tested end-to-end against HAPI FHIR v8.10.0-1 (October 2026) and work reliably:

* operation create <Resource> from "<fixture>" as create_resp
* operation search <Resource> as search_resp
* assert responseCode = 200
* assert resource = Bundle
* assert contentType = "application/fhir+json"
* assert expression "<fhirpath>"

Example:

Test: "Create and search"
  * operation create Patient from "p" as p1
  * assert responseCode = 201
  * operation search Patient as p_search
  * assert responseCode = 200
  * assert resource = Bundle
  * assert expression "Bundle.entry.where(resource.name.where(use='official')).count() > 0"

Known limits (do not use)

Pattern Why it does not run Workaround
read Patient/123 or read Patient/<id> The engine rejects the path client-side with HTTP 400; nothing reaches the server. Variable substitution ${fixture.id} is also never evaluated. Use search Patient as search_resp and filter with assertions; or switch to JSON TestScript with "params": "/123".
delete <Resource>/<id> Client-side HTTP 400. Use JSON TestScript or clean up via teardown with a fixture instead.
search Patient?family=Smith Search parameters cannot be encoded in TSH operation URLs; the whole string becomes operation.resource and the engine fails. Use JSON TestScript with "params": "?family=Smith". For large filter-heavy tests, prefer JSON.
${variableName} in operation URLs Variable substitution is not supported by the engine. Extract data into a variable for assertion logic; for request parameterization, use JSON TestScript.

Assertions

Assertions validate responses. The response references a prior operation's responseId.

* assert <type> = <value>
* assert <type> in (<value1>, <value2>, ...)
* assert expression "<fhirpath>"

Assertion types

Type Syntax Purpose
responseCode assert responseCode = 200 HTTP status code (exact match)
responseCode assert responseCode in (200, 201) HTTP status code (one of) — note: keeps only the first code
resource assert resource = Patient Response body type
contentType assert contentType = "application/fhir+json" HTTP Content-Type header
expression assert expression "<fhirpath>" FHIRPath query against the response
minimumId assert minimumId = "p1" Response id alphabetically >= the fixture id
validateProfile assert validateProfile = "http://example.org/Profile/MyPatient" Validate response against a profile (requires the profile to be loaded on the server)

Example assertions:

Test: "Example assertions"
  * operation create Patient from "p" as p1
  * assert responseCode = 201
  * assert resource = Patient
  * assert expression "Patient.name.count() > 0"
  * operation search Patient as search
  * assert responseCode = 200
  * assert resource = Bundle
  * assert contentType = "application/fhir+json"

Known limit: the in operator

assert responseCode in (200, 201) compiles but keeps only the first code (200) in the compiled JSON; the second is dropped silently. This is unverified upstream behaviour. Avoid relying on the in operator; instead, write separate assertions or use JSON TestScript.

Variables

Extract data from a response for use in later assertions:

* variable <name> = <fhirpath> from <responseId>

Example:

Test: "Extract a variable"
  * operation create Patient from "p" as p1
  * assert responseCode = 201
  * variable patient_id = resource.id from p1
  * operation search Patient as search
  * assert responseCode = 200
  * assert expression "Bundle.entry.where(resource.id=$patient_id).count() = 1"

In the compiled TestScript, this becomes a test.variable[] entry with: - name: "patient_id" - sourceExpression.expression: "resource.id" - sourceId: "p1" (the operation response to query)

Known limit: variable substitution in operation URLs (e.g. read Patient/${patient_id}) does not work. Variables are available only in assertions and later variable definitions.

Complete working example

TestScript: PatientCRUDTest
Title: "Patient CRUD Operations"
Description: "Create, search, and validate patient resources"
Status: #draft
Url: "http://example.org/TestScript/patient-crud"

Fixture: p1
  * resource = "patient-1.json"
  * autocreate = false
  * autodelete = false

Setup:
  * operation create Patient from "p1" as patient_created
  * assert responseCode = 201
  * assert resource = Patient

Test: "Search by family name"
  * operation search Patient as search_resp
  * assert responseCode = 200
  * assert resource = Bundle
  * assert expression "Bundle.entry.count() >= 1"

Teardown:
  * operation search Patient as final_search

Compilation and validation

Before running a test, validate and compile it:

# Validate the TSH syntax
frog_tsh_validate(path: "my-test.tsh")

# Compile to JSON TestScript
frog_tsh_compile(path: "my-test.tsh", output: "my-test.json")

# Lint the compiled JSON
frog_testscript_lint(path: "my-test.json")

Common validation errors

Error Cause Fix
Unknown operation type Typo in * operation ... Check spelling: create, search, read, etc.
Resource not found (in fixture) Fixture file does not exist or path is wrong Check the path relative to the TestScript; use frog_check_ig to find the workspace root
No fixture declared Referenced fixture (e.g. from "p1") has no matching Fixture: block Declare the fixture at the top level
Duplicate fixture id Two Fixture: declarations with the same name Rename one
No Test phase Zero Test: blocks Add at least one Test: block

More examples

See the FHIR Frog skill reference for worked examples: - skill/fhir-frog/examples/workflow-ig-developer.md — scaffold, author, validate, compile - skill/fhir-frog/references/tsh-cheatsheet.md — full grammar and edge cases - Example IGs: examples/hello-test-ig/ in this repository

See also