Getting started¶
This guide takes you from nothing to a passing suite in seven steps.
Once it passes, read Writing a Suite โ it covers the habits that make a suite worth trusting, in particular asserting the effects your application produces rather than only the response it returns.
Prerequisites¶
- Docker โ tomato starts your test containers
- Go 1.26+ โ only for the
go installand from-source methods
1. Install tomato¶
Quick install (recommended)¶
Using Go¶
Using Homebrew¶
Verify installation¶
2. Create a project¶
Create a new project with the init command:
This creates:
- tomato.yml - Main configuration file
- features/ - Directory for your feature files
Project layout¶
my-project/
โโโ tomato.yml # Main configuration
โโโ features/
โ โโโ example.feature # Gherkin test files
โโโ fixtures/ # Optional: SQL scripts, test data
3. Describe your dependencies¶
tomato.yml says what your service needs and how the scenarios reach it. The
examples on this page test an orders service that stores orders in Postgres
and publishes them to Kafka:
version: 2
# Infrastructure tomato starts, named after the software
containers:
postgres:
image: postgres:16-alpine
env: { POSTGRES_USER: test, POSTGRES_PASSWORD: test, POSTGRES_DB: orders }
ports: ["5432/tcp"]
wait_for: { type: exec, target: "pg_isready -h 127.0.0.1 -U test" }
kafka:
preset: kafka
# Your service
app:
command: ./orders
port: 8080
ready: { type: http, path: /health }
env:
DATABASE_URL: "postgres://test:test@{{.postgres.host}}:{{.postgres.port.5432}}/orders"
KAFKA_BROKERS: "{{.kafka.host}}:{{.kafka.port.9092}}"
# What the scenarios talk to, named <whose>-<role>
resources:
orders-client: # calls the orders service
type: http
base_url: http://localhost:8080
orders-db: # the orders service's database
type: postgres
container: postgres
database: orders
options: { user: test, password: test }
orders-events: # the topics the orders service uses
type: kafka
container: kafka
options: { topics: [orders.placed, payments] }
features:
paths: [./features]
Name resources by whose they are and what they do¶
A step starts with a resource name, and a reader sees the step long before the config. So the name says whose it is and what role it plays:
| Role | Name | Reads as |
|---|---|---|
| calls your service | orders-client |
"orders-client" sends "POST" to "/orders" |
| your service's data | orders-db, orders-cache |
"orders-db" table "orders" has "1" rows |
| a broker | orders-events (Kafka), orders-queue (RabbitMQ) |
"orders-events" receives from "orders.placed" |
| stands in for a service you call | stripe-stub, catalog-stub |
"stripe-stub" received "POST" "/v1/charges" |
| object storage | invoices-storage |
"invoices-storage" object "โฆ" should exist |
Not api, db, mock or http: those say what kind of thing it is, not whose.
Naming resources has the full
convention.
4. Write scenarios¶
Features, scenarios and steps¶
# One feature file per capability of your service
Feature: Placing orders
# The state every scenario below starts from
Background:
Given "orders-db" table "customers" has values:
| id | email |
| 7 | ada@example.com |
# One behaviour, named as a fact
Scenario: A placed order is stored and announced
# When: the one action
When "orders-client" sends "POST" to "/orders" with json:
"""
{"customer_id": 7, "sku": "TMT-01", "qty": 2}
"""
# Then: what it caused
Then "orders-client" response status is "201"
And "orders-db" table "orders" contains:
| customer_id | sku | qty | status |
| 7 | TMT-01 | 2 | pending |
And "orders-events" receives from "orders.placed" within "5s"
- Feature: a file in
features/, about one capability of your service. - Scenario: one behaviour of that capability, named after the outcome.
- Background: the starting state every scenario in the file shares. It runs after tomato's reset, before each scenario.
- Given sets up state, When is the one action, Then checks what it caused.
Principles¶
1. One scenario, one behaviour. One When. A scenario that also reads the
order back and cancels it fails for three reasons, and tells you none of them.
2. Check the effects, not only the response. A 201 says the service
didn't crash. The row in the database, the message on the topic and the file in
the bucket are what the rest of your system relies on, so assert them:
Then "orders-client" response status is "201"
And "orders-db" table "orders" has "1" rows
And "orders-events" receives from "orders.placed" within "5s"
# with an s3 resource for the invoices bucket
And "invoices-storage" object "invoices/inv-1.pdf" should exist
3. Split a flow where it crosses a boundary. When the service stores an order, publishes an event, and later consumes another event to mark the order paid, that's two behaviours, so two scenarios: one ends where the service publishes, the next starts where tomato publishes the event the service consumes.
Tempting: one scenario places the order, waits for the event, publishes the payment and checks the order is paid. It fails for any of four reasons, waits on asynchronous work, and the consumer can't be tested on its own.
Scenario 1 places an order and checks what the service did: the row, and the event on orders.placed. Scenario 2 seeds a pending order, has tomato publish the payment, and checks the service marks it paid. They meet at the topic, and each fails for one reason.
Scenario: Placing an order stores it and announces it
When "orders-client" sends "POST" to "/orders" with json:
"""
{"customer_id": 7, "sku": "TMT-01", "qty": 2}
"""
Then "orders-client" response status is "201"
And "orders-db" table "orders" has "1" rows
And "orders-events" receives from "orders.placed" within "5s"
Scenario: A payment marks the order paid
Given "orders-db" table "orders" has values:
| id | customer_id | sku | qty | status |
| 1 | 7 | TMT-01 | 2 | pending |
When "orders-events" publishes json to "payments":
"""
{"order_id": 1, "status": "succeeded"}
"""
Then "orders-db" query "SELECT status FROM orders WHERE id = 1" returns within "10s":
| status |
| paid |
4. Build the state you need. tomato empties every database and topic
before every scenario. A scenario that needs an order seeds one in its
Given or Background; it never relies on another scenario having run.
5. Variations are more scenarios, not a longer one. The out-of-stock case,
the invalid request and the provider being down are behaviours too. Give each
its own scenario, or a Scenario Outline when only the values change:
Scenario Outline: An invalid order is refused and nothing is stored
When "orders-client" sends "POST" to "/orders" with json:
"""
<body>
"""
Then "orders-client" response status is "400"
And "orders-db" table "orders" is empty
Examples:
| body |
| {"sku": "TMT-01", "qty": 2} |
| {"customer_id": 7, "qty": 0} |
Writing a Suite goes deeper into each of these.
5. Check it before you run it¶
Before running tests, validate your configuration and feature files:
This checks: - Configuration file syntax and structure - Resource type validity - Container references - Feature file parsing - Step definitions availability
Use --plain for CI environments without interactive output:
6. Run it¶
Run all tests:
Run with specific tags:
Run with verbose output:
7. Watch it run¶
tomato ui opens a local web page that lists the feature files, shows each
scenario's steps with the resource every step runs against, and runs scenarios
on demand. It watches the feature files and reloads as they change.
It is a development aid, not part of the stable surface: see Stability.
Pointing tomato at your application¶
Tomato can also start your application and connect it to test containers:
# tomato.yml
app:
command: go run ./cmd/server
port: 8080
ready:
type: http
path: /health
wait: 5s
env:
DATABASE_URL: "postgres://test:test@{{.postgres.host}}:{{.postgres.port.5432}}/test"
REDIS_URL: "redis://{{.redis.host}}:{{.redis.port.6379}}"
The {{.container.host}} and {{.container.port.<port>}} templates are replaced
with the container's address and its mapped host port. The number is the port
inside the container, so {{.postgres.port.5432}} resolves to whichever host
port Docker mapped 5432 to. A bare {{.postgres.port}} is left untouched.
Where to go next¶
- Writing a Suite โ one behaviour per scenario, and asserting the effects rather than just the response. Read this before you write the second scenario.
- Configuration Reference โ every
tomato.ymlfield - Step Reference โ every step, grouped by resource
- Test Coverage โ which lines of your service the scenarios ran
- GitHub Action โ running the suite in CI