Common things developers test
Does my service actually start and serve?
Section titled “Does my service actually start and serve?”Build the image, start it, wait for readiness, then assert on real behaviour — not a mock.
suite: service smoke testnodes: - 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: 60tests: - 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: 0Note: 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.
Does my config deploy correctly?
Section titled “Does my config deploy correctly?”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: 0Both 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 installnodes: - 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.