Guide

Writing Test Files

Complete guide to Tarn's YAML test format: test file structure, requests, variables, assertions, captures, cookies, multipart, and includes.

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 description
  • tags — Tags for filtering with --tag
  • env — Inline environment variables
  • cookies — Cookie handling policy: "auto" (default), "off", or "per-test"
  • setup — Steps run before all tests
  • teardown — Steps run after all tests
  • tests — Named test groups with their own steps
  • steps — Flat (unnamed) steps (use this OR tests)
Best practice

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 an include directive
  • {{ $builtin }} — Built-in function

Priority chain (highest to lowest):

  1. --var flag (CLI)
  2. Shell environment variables
  3. tarn.env.local.yaml
  4. tarn.env.{name}.yaml (with --env name)
  5. tarn.env.yaml
  6. 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.yaml for local dev secrets (add to .gitignore)
  • Pass secrets via CI env vars or --var
  • Reference shell env vars in YAML: use $VAR_NAME or ${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.

Common mistakes
  • JSONPath $.id on an array response — use $[0].id or $[?(@.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-Cookie headers are captured automatically
  • Cookie headers 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 with name and value
  • files — Array of file uploads with name, path (relative to test file), and optional content_type
Common mistakes
  • File paths are relative to the test file, NOT the working directory
  • Missing content_type defaults to application/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