Tales

Integration testing, reinvented

End-to-end tests, written once, replayable forever — in a single Go binary.

Tales is the integration testing tool we wished existed. No Python toolchain to babysit. No DSL that turns into a JavaScript codebase. One declarative HCL2 syntax, one seedable run, one tool for API, SQL, and iOS workflows.

~/projects/api — bash
$ tales test ./e2e/pass --seed 1234 --parallel 4
tales: loaded 12 scenarios from 5 files; timeout=disabled
 PASS  e2e/pass/blog.tales / Create blog post (842ms)
 PASS  e2e/pass/keyword.tales / Use keyword (231ms)
 PASS  e2e/pass/sql.tales / PostgreSQL operations (95ms)
 PASS  e2e/pass/file_upload.tales / Multipart upload (47ms)
 PASS  e2e/pass/signed_webhook.tales / Signed webhook (38ms)

Summary: 12 passed · 0 failed · 0 skipped · 1.24s

Why Tales exists

Built after years of fighting the same problems with Robot Framework and Karate.

No Python env to babysit

Robot Framework drags a Python toolchain that breaks on every OS update, every pip upgrade, every CI runner refresh. Tales is one static Go binary you drop into CI and forget about.

No DSL-meets-JavaScript creep

Karate scenarios tend to grow JavaScript blocks until they are a codebase. Tales is fully declarative HCL2 with built-in functions, generators, and matchers — what you write is what you read.

API, SQL, iOS in one runner

Stop juggling separate tools for HTTP, database fixtures, and mobile UI tests. Tales runs them in the same scenario file, with the same syntax, in the same report.

What you get out of the box

A focused toolset for the test problems that actually slow teams down.

Single binary

Drop `tales` into your CI. No runtime, no plugins, no version manager. Static Go binary for Linux and macOS.

Declarative HCL2

Readable scenarios that diff cleanly. The DSL is the test — no callbacks, no glue code, no JavaScript escape hatch.

Deterministic faker

Generate emails, passwords, people, MAC addresses, bytes. Same seed → same data, every run, on every machine.

Seedable replay

Reproduce a flaky CI failure locally with one flag. `--seed 1234` and your laptop replays exactly what the runner saw.

HTTP / SQL / iOS providers

Drive your API, seed your database (Postgres, MySQL), tap through a real iOS simulator — all from one tool.

Parallel by default

Scenarios run concurrently with `--parallel`. Steps inside a scenario stay sequential, so chained captures remain deterministic.

Visual HTML report

A self-contained HTML report with timeline, action tiles, and screenshot replay. Open it. Share it. Debug in two clicks.

CI-native outputs

JUnit XML and JSONL out of the box. Exit codes your pipeline already understands. No glue scripts, no reporters to wire up.

See it in 30 seconds

Three tabs — a scenario, the command that runs it, the report your CI gets.

version = 1

generator "email" "user_email" {
  prefix = "qa-"
  domain = "example.com"
}

scenario "Create blog post" {
  step "http" "create_user" {
    request {
      method = "POST"
      url    = "https://api.example.com/users"
      body {
        json = {
          email    = generate("user_email")
          password = "Sup3rS3cret!"
        }
      }
    }

    expect {
      status = 201
      json = {
        id    = is_string()
        email = request.body.json.email
      }
    }

    capture {
      id    = response.json.id
      email = response.json.email
    }
  }

  step "http" "create_post" {
    request {
      method  = "POST"
      url     = "https://api.example.com/blog/posts"
      headers = { Author = result.create_user.id }
      body {
        json = {
          title = "Hello from Tales"
          body  = "Reproducible test data, every run."
        }
      }
    }

    expect {
      status = 201
    }
  }

  teardown {
    step "http" "delete_user" {
      when = can(result.create_user.id)
      request {
        method = "DELETE"
        url    = "https://api.example.com/users/${result.create_user.id}"
      }
      expect { status = one_of([200, 204, 404]) }
    }
  }
}

Built for real-world test problems

Pick a starting point — every use case is one binary away.

API

HTTP workflows

Chain requests with captured IDs, assert JSON with matchers, sign webhooks with HMAC, upload multipart files. The HTTP provider is the heart of Tales.

step "http" "send_signed_webhook" {
  vars {
    ts   = now_unix()
    body = jsonencode({ id = "evt-1", type = "ping" })
    sig  = hmac_sha256_hex(config.webhook_secret, "${vars.ts}.${vars.body}")
  }

  request {
    method  = "POST"
    url     = "${config.base_url}/webhook"
    headers = { X-Signature = "t=${vars.ts},v1=${vars.sig}" }
    body { raw = vars.body }
  }
}
SQL

Database fixtures

Set up and tear down database state alongside your HTTP scenarios. Postgres and MySQL, scalar args with int64 precision, DSNs masked in reports.

config {
  sql {
    connections {
      pg = { driver = "postgres", dsn = env("POSTGRES_DSN") }
    }
  }
}

step "sql" "insert_org" {
  connection = "pg"
  exec {
    sql  = "INSERT INTO orgs (id, vip) VALUES ($1, $2)"
    args = ["org_123", true]
  }
  expect { json = { rows_affected = 1 } }
}
Mobile

iOS smoke tests

Drive a real iOS simulator with an embedded XCUITest driver — zero Swift code to write, no test target to maintain. Visual report shows every tap.

step "mobile" "fill_login" {
  platform = "ios"
  target   = "iphone"
  actions {
    input_text {
      id    = "login.email"
      value = "[email protected]"
    }
    input_text {
      id     = "login.password"
      value  = "secret"
      secure = true
    }
    tap { id = "login.submit" }
    wait_visible { id = "home.screen" }
  }
  expect {
    visible { id = "home.welcome" }
    text {
      id    = "home.user"
      value = contains("Welcome")
    }
  }
}

Same seed. Same data. Every run.

Tales generators are seeded — pass `--seed 1234` once and your CI gets the same emails, passwords, person names, and IDs as your laptop. No more "works on my machine". No more rerunning a CI job five times hoping the flake goes away.

  • A single `--seed` flag controls every faker call across every scenario.
  • Generator outputs are mixed with scenario, step, and generator names — so identical runs produce identical values even under `--parallel`.
  • Reproduce a red CI build by copying its seed into your local command line. The data lines up byte for byte.
# first run, today, on your laptop
$ tales test ./e2e/pass --seed 1234
[email protected] · Sup3rS3c! · Alice Martin
# second run, six months later, in CI
$ tales test ./e2e/pass --seed 1234
[email protected] · Sup3rS3c! · Alice Martin
Identical. Byte for byte.

Install Tales

Two paths. Pick whichever fits your stack.

Pre-built binary

Grab the latest release tarball for Linux or macOS (amd64 / arm64) from GitHub Releases.

Open releases →

Build from source

You will need Go 1.26+. The Makefile handles the rest.

git clone https://github.com/hyperxlab/tales
cd tales
make install