Configuration reference¶
Complete reference for tomato.yml configuration options.
File structure¶
version: 2 # Required: config version
settings: # Test execution settings
timeout: 5m
fail_fast: false
output: pretty
containers:
reuse: false # keep containers between runs instead of starting fresh
app: # Application under test (optional)
command: ./my-app
port: 8080
ready:
type: http
path: /health
wait: 5s
env:
KEY: value
containers: # Container definitions
name:
image: image:tag
command: [] # Optional startup command (list or string)
env: {}
ports: []
volumes: []
depends_on: []
wait_for: {}
resources: # Resource/handler definitions
name:
type: http|http-server|grpc|postgres|scylladb|cassandra|redis|kafka|rabbitmq|s3|websocket|websocket-server|shell|aws|lifecycle
# aliases: postgresql, http-client, grpc-client, websocket-client, minio
container: container_name
options: {}
hooks: # Lifecycle hooks
before_all: []
after_all: []
before_scenario: []
after_scenario: []
features: # Feature file settings
paths:
- ./features
tags: ""
Settings¶
| Field | Type | Default | Description |
|---|---|---|---|
timeout |
duration | 5m |
Global test timeout |
fail_fast |
bool | false |
Stop on first failure |
output |
string | pretty |
Output formats, comma-separated; see Reports |
Reports¶
output takes one or more comma-separated formats. A format without a path
prints to the console; format:path writes that format to a file. Use this to
feed CI test reports and test-management tools.
| Format | Writes |
|---|---|
pretty |
Human-readable console output (default) |
progress |
One character per step |
junit |
JUnit XML, one testsuite per feature and one testcase per scenario |
cucumber |
Cucumber JSON, the input most Cucumber report tools import |
tomato |
Structured events used by the GitHub Action's PR comment |
Tomato creates the report directories if they don't exist. When --format is
passed on the command line (the GitHub Action passes --format tomato for PR
comments), it replaces the console format but file outputs from output are
still written.
App configuration¶
Configure your application under test to run with test containers.
app:
# Option 1: Run a command
command: go run ./cmd/server
workdir: ./
# Option 2: Build from Dockerfile
build:
dockerfile: Dockerfile
context: .
# Connection settings
port: 8080
ready:
type: http # http, tcp, or exec
path: /health # For HTTP checks
status: 200 # Expected status (default: 200)
timeout: 30s # Ready check timeout
# Wait after ready check passes
wait: 5s
# Environment variables (supports container templates)
env:
DATABASE_URL: "postgres://test:test@{{.postgres.host}}:{{.postgres.port.5432}}/test"
REDIS_URL: "redis://{{.redis.host}}:{{.redis.port.6379}}"
Port already in use
In command mode, tomato checks port before starting the app. If another
process is already listening there (a local make run, a leftover test
run), tomato fails instead of starting: otherwise the ready check would
pass against the other process and the tests would hit the wrong app.
In command mode the app inherits tomato's environment, plus env. Some
resources the app depends on while it starts are set up before it and add to
that environment too: the aws resource points the app's AWS SDK at its
STS and removes AWS_ACCESS_KEY_ID and friends inherited from your shell. The
app's own env still wins over anything a resource sets.
The app runs in its own process group, and stopping it stops the whole group,
so command: go run ./cmd/server (where go does not pass signals on) does not
leave the server running and holding the port.
Template variables¶
In app.env, you can use templates to inject container addresses:
| Template | Description |
|---|---|
{{.container_name.host}} |
Container hostname (localhost in command mode, the container's DNS name in container mode) |
{{.container_name.port.NNNN}} |
Host port that NNNN, the port inside the container, is mapped to |
{{.resource_name.url}} |
Base URL of an http-server resource |
NNNN is required. A bare {{.container_name.port}} is left in the value
verbatim, so the application receives the literal template text.
Example with PostgreSQL:
containers:
postgres:
image: postgres:15
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- "5432"
app:
command: ./my-app
env:
# These are resolved at runtime when containers start
DB_HOST: "{{.postgres.host}}"
DB_PORT: "{{.postgres.port.5432}}"
DATABASE_URL: "postgres://testuser:testpass@{{.postgres.host}}:{{.postgres.port.5432}}/testdb"
Note: The container field in resource definitions automatically handles host/port resolution - you don't need to specify connection strings manually for resources.
Containers¶
Define Docker containers for your test infrastructure.
containers:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- "5432"
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
depends_on:
- other_container
wait_for:
type: port
target: "5432"
timeout: 30s
volumes are source:target[:mode]. A source path (./x, ../x, /x, ~/x)
is bound into the container, relative paths resolved against the directory of
tomato.yml; a bare name is a Docker volume. build (context, dockerfile)
builds the image instead of pulling one, with context relative to tomato.yml
too. The image is named tomato-<container> and kept, so the next run rebuilds
from cache into the same image. Each container's output is written to .tomato/runs/<run>/container-<name>.log
for as long as it runs, including when it fails to start.
Presets¶
A preset is a container tomato knows how to run. It fills in the image, env,
ports and wait strategy; anything the entry sets itself (an env key, image,
wait_for) wins.
| Preset | What it runs |
|---|---|
kafka |
A single-node KRaft broker, reachable from the host and from other containers. With auth: aws_msk_iam, a second listener speaks SASL AWS_MSK_IAM like MSK's IAM port. See Kafka. |
Resources tomato does not manage¶
tomato resets every resource before every scenario, and that is not
configurable. A resource with no container: is a system tomato did not
start, so resetting it destroys someone's real data. tomato refuses to load
such a config:
resource "db" (postgres) has no container, so it points at a system tomato
does not manage — and tomato wipes every resource before every scenario,
which would destroy its data.
Give it a container: so tomato runs it, or, if it genuinely is a throwaway
system, say so:
resources:
db:
type: postgres
allow_destructive_reset: true # yes, wipe it before every scenario
options:
host: scratch-db.internal
The acknowledgement stops the refusal, not the warning: tomato validate
still flags it, and every run prints which unmanaged resources it is about to
wipe. Only the stateful types are affected — postgres, redis, kafka,
rabbitmq, s3, scylladb and their aliases. An http-client pointed at a
remote URL holds no data, so it needs nothing.
Container naming and reuse¶
Every container and network tomato creates is named and labelled, so docker ps
says whose they are:
tomato-<project>-<run>-<container> e.g. tomato-9a987f0c-06e8b19f-postgres
tomato-<project>-<run>-net the network they share
<project> is derived from the config file's path, so two projects on one
Docker host never collide; <run> is the run id, so two runs of the same
project can go at once. Labels tomato.managed, tomato.scope, tomato.run
and tomato.role are set on all of them, which is enough to find leftovers:
| Setting | Default | Meaning |
|---|---|---|
settings.containers.reuse |
false |
Keep containers between runs instead of starting fresh ones |
With reuse: true the run id drops out of the names, so a rerun finds what the
last run left behind and skips container startup entirely. Resets still run
before every scenario, so scenarios stay isolated from each other. What you
give up: a container an earlier run corrupted is no longer thrown away, and two
runs of the same project can no longer run at the same time. tomato leaves the
containers running when the suite ends and prints the command to remove them.
Wait strategies¶
| Type | Description | Fields |
|---|---|---|
port |
Wait for port to be ready | target |
log |
Wait for log message | target (regex) |
http |
Wait for HTTP endpoint | path, target (port), method |
exec |
Run command in container | target (command) |
Resetting state¶
Resetting is configured per resource, not per container — see
options.reset_strategy under Resources and on each resource's
page. How often it runs is not configurable: every resource is reset before
every scenario. A scenario that needs a starting state builds it in a
Background, which runs after the reset.
The opt-outs v2.3.0 removed, settings.reset, a resource's reset and
reset_strategy: none, are refused at load with this explanation. A config
written for an earlier release fails to load rather than having a resource it
used to leave alone wiped before every scenario.
Resources¶
Define handlers for test steps.
HTTP¶
| Option | Default | Description |
|---|---|---|
timeout |
30s |
Per-request timeout. For an event stream it bounds only the wait for the response headers; the stream stays open until the scenario closes it, and each receive step's within bounds its wait |
no_redirect |
false |
Return the redirect response instead of following it |
port |
8080 |
Container port the base URL is built from. Only with container |
scheme |
http |
Scheme for the base URL built from container |
health_path |
(none) | Path polled once at startup to check the service answers |
Request headers are set by steps ("api" header "Authorization" is "..."), not
here, and are cleared between scenarios.
PostgreSQL¶
resources:
db:
type: postgres
container: postgres
database: test
options:
user: test
password: test
# Only truncate these tables during reset (if not set, truncates ALL public tables)
tables:
- users
- orders
- products
# Never truncate these tables (added to the defaults below)
exclude:
- countries # reference data seeded by a migration
Reset behavior¶
By default, PostgreSQL resources truncate all tables in the public schema before each scenario. You can control this behavior:
| Option | Description |
|---|---|
tables |
If set, only these tables are truncated (instead of all) |
exclude |
Extra tables to never truncate, on top of the defaults |
Unless tables is set, migration history tables are never truncated, so
migration tools still see their migrations as applied:
| Tool | Tables |
|---|---|
| golang-migrate, Rails, sqlx | schema_migrations |
| goose | goose_db_version |
| Flyway | flyway_schema_history |
| Liquibase | databasechangelog, databasechangeloglock |
Reference data that migrations insert (countries, roles, permissions) is
truncated like any other table. Add those tables to exclude so scenarios
don't have to re-seed them.
tables and exclude are mutually exclusive: when tables is set, exactly
those tables are truncated and both exclude and the migration-table list
above are ignored.
The container field automatically provides the connection details - tomato resolves the container's host and port at runtime.
ScyllaDB / Cassandra¶
resources:
scylla:
type: scylladb # or: cassandra
container: scylla
options:
keyspace: app # created if missing, then used for unqualified tables
schema:
- ./fixtures/schema.cql
exclude:
- countries # reference data kept across scenarios
Every table in the keyspace is truncated before each scenario, except those in
exclude. See ScyllaDB Configuration for all options.
Redis¶
resources:
cache:
type: redis
container: redis
options:
db: 0
password: ""
reset_strategy: flush # flush or pattern
reset_pattern: "*" # For pattern strategy
Kafka¶
resources:
kafka:
type: kafka
container: kafka
options:
topics:
- events
- notifications
partitions: 1
replication_factor: 1
reset_strategy: delete_recreate
# Optional: Avro via Confluent Schema Registry
schema_registry:
container: schema-registry # or url: http://localhost:8081
| Field | Default | Description |
|---|---|---|
container |
— | The container tomato starts and connects to |
brokers |
— | Broker addresses, instead of container. A broker tomato does not start needs allow_destructive_reset: true, since its topics are recreated before every scenario |
See Kafka: Avro and Schema Registry.
WebSocket¶
resources:
ws:
type: websocket
url: ws://localhost:8080/ws
# Or use container
container: app
options:
port: "8080"
path: /ws
protocols:
- graphql-ws
headers:
Authorization: Bearer token
gRPC¶
resources:
grpc:
type: grpc
address: "localhost:9090" # or: container: myservice
options:
timeout: 30s
The server must register the gRPC reflection service. See gRPC Configuration.
S3¶
resources:
files:
type: s3
container: minio
options:
buckets:
- uploads
reset_strategy: purge # purge or delete
See S3 Configuration for MinIO and LocalStack setup.
Shell¶
Hooks¶
Execute actions at different lifecycle points.
hooks:
before_all:
- sql_file: ./fixtures/schema.sql
resource: db
- shell: ./scripts/setup.sh
after_all:
- shell: ./scripts/cleanup.sh
before_scenario:
- sql: "DELETE FROM events"
resource: db
after_scenario:
- exec: redis-cli FLUSHDB
container: redis
Hook types¶
| Type | Description |
|---|---|
sql |
Execute SQL query |
sql_file |
Execute SQL file |
shell |
Run shell command |
exec |
Run command in container |
Features¶
Configure feature file discovery.
Tag expressions¶
| Expression | Description |
|---|---|
@smoke |
Scenarios tagged with @smoke |
@smoke and @api |
Both tags |
@smoke or @api |
Either tag |
not @slow |
Exclude slow tests |
@smoke and not @wip |
Smoke tests, excluding WIP |
(@smoke or @api) and not @slow |
Parentheses group terms |
not binds tighter than and, which binds tighter than or. godog's legacy
syntax (@smoke && ~@slow, , for or) is still accepted.
Environment variables¶
Use environment variables anywhere in the config:
Only $VAR and ${VAR} are expanded. Shell default syntax such as
${VAR:-15} is not supported: the whole VAR:-15 is read as the variable
name, so the value expands to an empty string. Set the variable, or keep the
default in tomato.yml.