Skip to content

Common things developers test

Build the image, start it, wait for readiness, then assert on real behaviour — not a mock.

suite: service smoke test
nodes:
- name: app
type: docker
options:
image: myservice:latest
ports: ["8080:8080"]
setup:
- name: wait for the API to answer
node: app
step:
type: wait_for
options:
command: curl -sf http://localhost:8080/health
timeout: 60
tests:
- name: health endpoint reports healthy
node: app
type: http_request
options:
url: http://localhost:8080/health
evaluate:
status_code: 200
json_path: { path: status, equals: healthy }
- name: service survives a restart
node: app
type: execute
options:
command: kill -HUP 1 && sleep 2 && curl -sf http://localhost:8080/health
evaluate:
exit_code: 0

Note: the network checks — the http_request, port_check, and tls_cert tests, and the http_request and dns_request steps — probe from the node they name. That is what makes them mean what they read as: the test above reaches localhost from inside the container, the same namespace the wait_for step’s curl runs in. A from option chooses the vantage point, and from: host asks the same question from the machine running DART — which is what the published ports: ["8080:8080"] makes possible, and what you want for an endpoint that must be reachable from CI.

The node-side probes are shell based and depend on tools being present in the image: http_request requires curl, tls_cert requires openssl, dns_request uses whichever of getent, dig, host, or nslookup it finds, and port_check prefers bash’s /dev/tcp and falls back to nc. Each fails loudly when its tool is absent rather than reading as an unreachable endpoint. A minimal image that ships none of them is a case for from: host.

Warning: a docker node’s image must run a long-lived foreground process. DART creates the container from the image’s own CMD/ENTRYPOINT with no TTY and no attached stdin, so an image whose default command is an interactive shell — ubuntu, debian, alpine — exits the moment it starts. Node setup then polls for up to two minutes waiting for the container to report running and fails with timeout waiting for container ... to become ready. Give such an image a command: that stays up (command: ["sleep", "infinity"]), use a purpose-built service image, or put it on an lxd (or lxd-vm) or ssh node instead.

Render a template, push it to the node, and verify what actually landed.

setup:
- name: render the config for this node
node: app
step:
type: file_template
options:
source: fixtures/app.conf.tmpl # Go template: {{ .port }}
dest: /etc/myapp/app.conf
overwrite: true
create_dir: true # mkdir -p the parent directory
mode: "0640"
values: { port: 8080, workers: 4 }
tests:
- name: the deployed config is what we intended
node: app
type: file_content
options:
filename: /etc/myapp/app.conf
evaluate:
contains: "port = 8080"
- name: the service accepts it
node: app
type: execute
options:
command: myservice --config /etc/myapp/app.conf --validate
evaluate:
exit_code: 0

Both create_dir and overwrite default to false: without them the step fails when the parent directory is missing, and fails again when the destination already exists.

Note: every local path a suite writes follows one rule — absolute paths are used as-is, ~ is the invoking user’s home directory, and anything else is relative to the directory holding the suite file. That covers file-step sources and destinations, docker volumes, LXD disk sources, SSH keys and known_hosts, LXD certificates, compose_file, docker.images[].dockerfile, and !!load_from. A suite is therefore portable: it behaves the same run from the repository root, from its own directory, or from a CI checkout elsewhere.

Is the package installable on a clean machine?

Section titled “Is the package installable on a clean machine?”

The classic “works on my machine” bug: a dependency you have installed and your users don’t.

suite: clean install
nodes:
- name: clean
type: lxd # boots an init system, so services can start
options: { image: ubuntu:24.04 }
setup:
- name: copy the built package in
node: clean
step:
type: file_push
# source is read on the machine running DART, relative to the suite file
options: { source: dist/myservice.deb, dest: /tmp/myservice.deb }
- name: install it
node: clean
step:
type: execute
options: { command: "apt-get install -y /tmp/myservice.deb" }
tests:
- name: the service starts on a machine that has nothing else
node: clean
type: service_status
options: { service: myservice }

Note: service_status runs systemctl is-active <service> on the node and compares the output to evaluate.status (default active), so it requires systemd on the target. LXD/Incus containers and VMs and SSH hosts qualify; docker nodes generally do not, since standard base images carry no systemctl and DART does not boot containers under an init system. The equivalent check on a docker node is type: execute running the service’s own health or version command.

Run the same suite against several nodes at once by listing them: node: [ubuntu, debian, alpine] expands into one test per node.