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 tofalse.autodelete: iftrue, the fixture resource is deleted after the test. Deletion uses the resource'sidfield, 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:
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.
Operations¶
An operation represents a FHIR interaction.
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:
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¶
- TestScript JSON authoring — detailed cheat sheet of every TSH token
- Choosing a TestScript format — TSH vs JSON vs FSH Instance
- FHIR TestScript specification