Skip to content

Introduction

Tales is a declarative integration and end-to-end testing tool packaged as a single Go binary. You write tests as .tales files using the readable HCL2 syntax, and a single tales test command runs them with deterministic, seedable data generation.

Tales is an open-source alternative to Robot Framework, Karate, and Venom, with one runner for HTTP, SQL and mobile, no Python toolchain, no JavaScript creep, and no YAML soup.

You’ll feel at home in Tales if you have ever:

  • inherited a Robot Framework suite and spent more time keeping Python deps alive than writing tests;
  • watched a Karate scenario quietly grow into a JavaScript codebase;
  • pushed a Venom YAML scenario past the point where YAML still feels declarative;
  • maintained three separate test stacks (HTTP, database, mobile) that each demand a different runner;
  • chased a flaky CI failure that you could never reproduce locally.

Tales is designed to remove all those daily frictions.

API workflows

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

Database hooks

Run plain SQL statements (Postgres or MySQL) inside a scenario to flip an internal flag, seed a row, or read state your public API does not expose. Not a fixture loader, not a migration tool, just a thin escape hatch alongside HTTP.

iOS smoke tests

Drive a real iOS simulator with an embedded XCUITest driver. Zero Swift code to write. The visual HTML report shows every tap, swipe, and screenshot.

Reusable flows

Extract login, signup, or any common sequence into a keyword with typed inputs and named outputs. Call it from any scenario.

generator "email" "user_email" {
prefix = "qa-"
domain = "example.com"
}
scenario "Create user" {
step "http" "register" {
request {
method = "POST"
url = "${config.base_url}/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
}
}
}

Three things to notice:

  1. Generators are declared once and reused. generate("user_email") produces a stable value for the same seed.
  2. The DSL is fully declarative. No glue language, no callbacks: expect, capture, and assertions are all expressions.
  3. Captures feed downstream steps. Later steps read result.register.id to chain workflows without writing imperative code.

Determinism is a feature, not a side effect

Section titled “Determinism is a feature, not a side effect”

Tales generators are seeded. Two runs with the same --seed produce byte-identical generated values. This is what lets you reproduce a flaky CI run on your laptop with one flag:

Terminal window
tales test ./e2e/pass --seed 1234

The seed is mixed with the scenario name, the step name, the generator name, and the expression path, so identical runs produce identical values even under --parallel. See the Deterministic test data guide.