1. Test File Format
A .tarn.yaml file has this structure:
# yaml-language-server: $schema=https://raw.githubusercontent.com/NazarKalytiuk/tarn/main/schemas/v1/testfile.json name: User API description: CRUD tests for the user endpoint tags: [smoke, regression] env: base_url: "http://localhost:3000" cookies: "auto" setup: - include: ./shared/auth.tarn.yaml tests: - name: Create user steps: - name: POST /users request: method: POST url: "{{ env.base_url }}/users" headers: Content-Type: "application/json" body: name: "Jane Doe" capture: user_id: "$.id" assert: status: 201 body: "$.name": "Jane Doe" teardown: - name: Cleanup request: method: DELETE url: "{{ env.base_url }}/users/{{ capture.user_id }}"
Key structural elements:
name— Suite name (recommended for readability)description— Optional suite descriptiontags— Tags for filtering with--tagenv— Inline environment variablescookies— Cookie handling policy: "auto" (default), "off", or "per-test"setup— Steps run before all teststeardown— Steps run after all teststests— Named test groups with their own stepssteps— Flat (unnamed) steps (use this ORtests)
Use tests (named test groups) for most suites. Use flat steps for simple health-check files.
2. Requests
HTTP method and URL:
request: method: GET url: "{{ env.base_url }}/health"
With headers:
request: method: POST url: "{{ env.base_url }}/users" headers: Content-Type: "application/json" Authorization: "Bearer {{ capture.token }}"
JSON body:
request: method: POST url: "{{ env.base_url }}/users" body: name: "Jane Doe" email: "jane@example.com" role: "admin"
Plain text body:
request: method: POST url: "{{ env.base_url }}/echo" headers: Content-Type: "text/plain" body: "Hello, world!"
Form-encoded body:
request: method: POST url: "{{ env.base_url }}/login" form: username: "admin" password: "secret123"
GraphQL:
request: method: POST url: "{{ env.base_url }}/graphql" graphql: query: | query GetUser($id: ID!) { user(id: $id) { name email } } variables: id: "{{ capture.user_id }}"
Auth (Bearer token):
request: method: GET url: "{{ env.base_url }}/protected" auth: bearer: "{{ capture.token }}"
Auth (Basic):
request: method: GET url: "{{ env.base_url }}/basic-auth" auth: basic: username: "admin" password: "pass"
Query parameters are embedded in the URL:
url: "{{ env.base_url }}/users?page=1&limit=50"
Request methods: Any valid HTTP method works: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, etc.
3. Variables & Interpolation
Variables are referenced with {{ }} syntax:
url: "{{ env.base_url }}/users/{{ capture.user_id }}"
Variable types:
{{ env.x }}— Environment variable from any env source{{ capture.x }}— Captured value from a previous step{{ params.x }}— Parameter from anincludedirective{{ $builtin }}— Built-in function
Priority chain (highest to lowest):
--varflag (CLI)- Shell environment variables
tarn.env.local.yamltarn.env.{name}.yaml(with--env name)tarn.env.yaml- Inline
env:block in the test file
Capture transforms (pipe syntax):
url: "{{ capture.tags | first }}" url: "{{ capture.tags | join('|') }}" url: "{{ capture.message | split(' ') | last }}" url: "{{ capture.count | to_int }}" url: "{{ capture.id | to_string }}" url: "{{ capture.text | replace('old', 'new') }}"
Available transforms: first, last, count, join(delimiter), split(delimiter), replace(from, to), to_int, to_string.
4. Environment Files
tarn.env.yaml (committed to repo, non-sensitive defaults):
base_url: "http://localhost:3000" timeout: 10000
tarn.env.local.yaml (NOT committed, local overrides):
base_url: "http://localhost:3000" api_key: "local-dev-key-123"
Named environments (tarn.env.prod.yaml, tarn.env.staging.yaml):
# tarn.env.prod.yaml base_url: "https://api.example.com" api_key: "${PROD_API_KEY}"
Run with: tarn run --env prod
Secrets management:
- NEVER commit secrets to
tarn.env.yaml - Use
tarn.env.local.yamlfor local dev secrets (add to .gitignore) - Pass secrets via CI env vars or
--var - Reference shell env vars in YAML: use
$VAR_NAMEor${VAR_NAME}in values
Configuration (tarn.config.yaml):
test_dir: "tests" timeout: 10000 retries: 0 parallel: false fail_fast_within_test: false faker: seed: 42
5. Captures
Captures extract values from responses for use in subsequent steps.
JSONPath capture (shorthand):
capture: user_id: "$.id" user_name: "$.name" roles: "$.roles[*].name"
Extended capture sources:
capture: # From response header session_id: header: "set-cookie" regex: "session=([^;]+)" # From cookie auth_token: cookie: "token" # From whole body (text/HTML responses) page_content: body: true # From status code response_status: status: true # From final URL (after redirects) final_url: url: true # Explicit JSONPath created_at: jsonpath: "$.createdAt"
Optional captures and defaults:
capture: user_id: jsonpath: "$.id" optional: true default: "fallback-id"
Status-gated captures:
capture: user_id: jsonpath: "$.id" when: status: 201
Type-preserving behavior: Numbers are captured as numbers, booleans as booleans, strings as strings. JSONPath capture preserves the JSON type of the matched value.
- JSONPath
$.idon an array response — use$[0].idor$[?(@.name=="Alice")].id - Header names are case-insensitive in lookup but should match the actual header name
regex:uses capture group 1 — make sure your regex has a capturing group in parentheses
6. Assertions
Full reference: Assertion Operators Reference
Status assertions:
assert: status: 200 # exact status: "2xx" # any 2xx status: { in: [200, 201, 204] } status: { gte: 200, lt: 300 }
Body assertions via JSONPath:
assert: body: "$.name": "Jane Doe" # exact match "$.id": { type: string, not_empty: true } # multiple operators (AND) "$.email": { contains: "@" } # substring "$.role": { in: ["admin", "user"] } # value in list "$.createdAt": { is_date: true } # format validator "$.count": { gt: 0 } # numeric comparison "$.tags": { length: 3 } # array length "$.id": { is_uuid: true } # UUID format
Header assertions:
assert: headers: content-type: "application/json" x-request-id: { matches: "^[a-f0-9-]+$" }
Duration assertions:
assert: duration: "< 500ms" duration: "<= 1s" duration: "> 200ms"
Plain text / HTML response:
assert: body: "$": "expected plain text" "$": { contains: "partial match" }
Detailed reference of all 30+ assertion operators is on the Assertion Operators Reference page.
7. Built-in Functions
Full reference: Built-in Functions Reference
body: idempotency_key: "{{ $uuid }}" transaction_id: "{{ $uuid_v7 }}" name: "{{ $name }}" email: "{{ $email }}" random_tag: "{{ $random_hex(8) }}" count: "{{ $random_int(1, 100) }}" role: "{{ $choice(admin, user, viewer) }}" slug: "{{ $slug }}" timestamp: "{{ $now_iso }}"
Deterministic seeding:
$ TARN_FAKER_SEED=42 tarn run
Or in tarn.config.yaml:
faker: seed: 42
See Built-in Functions Reference for the full list.
8. Cookies
Automatic cookie handling is ON by default:
Set-Cookieheaders are captured automaticallyCookieheaders are sent on subsequent requests- The cookie jar is shared across all steps in the file
Disable cookies:
cookies: "off"
Named cookie jars (for multi-user sessions):
tests: - name: Admin session steps: - name: Login as admin request: method: POST url: "{{ env.base_url }}/login" body: { username: "admin", password: "admin123" } cookies: "admin" - name: Admin action request: method: GET url: "{{ env.base_url }}/admin/dashboard" cookies: "admin" - name: User session steps: - name: Login as user request: method: POST url: "{{ env.base_url }}/login" body: { username: "user", password: "user123" } cookies: "user"
Per-step override:
steps: - name: No cookies for this step request: method: GET url: "{{ env.base_url }}/public" cookies: "off"
Per-test reset:
cookies: "per-test"
9. Multipart Upload
steps: - name: Upload file request: method: POST url: "{{ env.base_url }}/upload" multipart: fields: - name: "title" value: "My Photo" - name: "description" value: "A sample image upload" files: - name: "file" path: "./fixtures/test.jpg" content_type: "image/jpeg" - name: "thumbnail" path: "./fixtures/thumb.png" content_type: "image/png" headers: Authorization: "Bearer {{ capture.token }}"
Field reference:
fields— Array of text fields withnameandvaluefiles— Array of file uploads withname,path(relative to test file), and optionalcontent_type
- File paths are relative to the test file, NOT the working directory
- Missing
content_typedefaults toapplication/octet-stream - File must exist at the specified path
10. Includes
Share reusable steps across test files:
shared/auth.tarn.yaml:
name: Auth steps: - name: Login request: method: POST url: "{{ env.base_url }}/auth/login" body: username: "{{ params.username }}" password: "{{ params.password }}" capture: token: "$.token" assert: status: 200 body: "$.token": { not_empty: true }
Usage in a test file:
setup: - include: ./shared/auth.tarn.yaml with: username: "admin" password: "{{ env.admin_password }}" tests: - name: Authenticated flow steps: - name: Get profile request: method: GET url: "{{ env.base_url }}/profile" auth: bearer: "{{ capture.token }}" assert: status: 200
Include placement:
- In
setup— for shared auth, data seeding - In
teardown— for shared cleanup - In
steps/tests— for reusable request patterns
Parameters: Use with: to pass {{ params.x }} values to the included file.
Override: Use override: to override env/cookies/defaults from the included file.
Folder structure best practice:
tests/ health.tarn.yaml users.tarn.yaml shared/ auth.tarn.yaml cleanup.tarn.yaml