Dartmole

Mock file format

A mock is one text file that says which requests it answers and what it answers with. This page is the complete set of rules: you should be able to write a valid mock from it without reading any code.

At a glance

# Captured from api.example.com/app/version on 2026-09-29.
name: GET /app/version
match:
  method: GET
  path: /app/version
delayMs: 800
---
HTTP 200
content-type: application/json

{
  "latest": "2.35.5",
  "minimum": "2.32.2"
}

From top to bottom:

  1. The rule, in YAML: which requests this mock answers, and a comment saying why it exists.
  2. A line that is exactly ---.
  3. The status line, HTTP 200.
  4. Response headers, one name: value per line.
  5. An empty line.
  6. The body, exactly as it should reach the app.

Parts 3 to 6 are written the way curl -i prints a response.

Where mocks live

  • Mocks live in .dartmole/mocks/ at the root of the app's repository.
  • A mock is a file ending in .mock. The loader ignores every other file in the folder, so README.md or a body file can sit next to the mocks.
  • The id of a mock is its filename without .mock: checkout-declined.mock has the id checkout-declined. Whether a mock is switched on is remembered on this machine, by id, so renaming a file switches that mock off.
  • Mocks are inert when they arrive. Nothing in the file says whether it is on.
  • The file is UTF-8. Line endings can be LF or CRLF.

The rule

The rule is everything above the first line that is exactly --- (no leading or trailing spaces). It is a YAML mapping, so # comments work there and are the place to say why a mock exists. The file starts directly with the YAML; do not open it with a --- line of its own, as Jekyll-style front matter does, because that line is taken to be the separator.

Key Type Default Meaning
name string the id What the desktop app shows for this mock.
match.method string any method GET, POST… Compared case-insensitively.
match.host string any host A host, optionally with * wildcards. No port and no scheme.
match.path string any path A path, optionally with * wildcards. No query string.
match.query mapping any query Query parameters the request must have. See Query parameters.
delayMs integer ≥ 0 0 How long to wait before answering, in milliseconds.
bodyFile string — Read the body from this file instead. See Bodies from a file.

Every key is optional, and an empty rule is valid (it matches every request). Unknown keys are ignored so that a newer file still loads in an older Dartmole. Mind the spelling: a misspelt mach: is ignored like any unknown key, and the mock then matches every request.

Matching

  • An omitted field matches anything. Leave host out and the mock answers on production, staging, dev and localhost alike. That is usually what you want; name a host only when you mean just that one.
  • * matches zero or more characters, anywhere, including / and .. There is no **.
  • Host and path are compared case-insensitively.
  • Ports are not part of the host: localhost matches localhost:8080 and localhost:3000.
  • path never includes the query string. path: /search matches /search?q=cats; to require q, use query.
YAML
host: api.example.com    # that host only
host: "*.example.com"    # subdomains, not the apex
host: "*example.com"     # the apex too, since * also matches nothing
path: /v1/users/*        # /v1/users/42, /v1/users/42/orders, and /v1/users/

Quote any value that starts with *, because an unquoted * means something else in YAML.

Query parameters

YAML
match:
  path: /api/v2/tasks
  query:
    page: "2"          # page must be 2
    search: "*"        # search must be there, with any value
    sort: "name*"      # sort must start with name
    tag: [urgent, me]  # tag must appear with both values
  • Each listed parameter must be present with a matching value. Parameters not listed are ignored, so the mock above also answers ?page=2&search=x&sort=name&tag=me&tag=urgent&size=20.
  • No query matches any query string, including none.
  • * in a value is the same wildcard as in a path, and "*" on its own accepts any value, including an empty one, as long as the parameter is there.
  • A name with a list of values requires each of them, in any order.
  • Names and values are compared case-sensitively, because a query value is data: q: cat does not match ?q=Cat. They are compared after decoding, so q: black cat matches ?q=black%20cat and ?q=black+cat.
  • Quote numbers and anything starting with *. A bare 20 is read as the text 20 anyway, but "2" is what Dartmole writes.

When Dartmole captures a call, it writes none of the call's query parameters, because a query string is where tokens tend to be. The editor offers them as suggestions, and marks a name that looks like a credential, such as access_token, apiKey or sig.

When several mocks match

When several switched-on mocks match one request, the most specific one answers. Mocks are compared on:

  1. how many fields they set, counting each query parameter as one,
  2. how many of those fields have no *,
  3. how many characters of those fields, and of query names, are not *.

So /v1/users/42 beats /v1/users/*, which beats /v1/*; ?page=2 beats ?page=*, which beats no query at all. If two mocks are still level after all three, that is a conflict: Dartmole answers with neither, lets the request through to the real backend, and shows the conflict. File order never matters.

The response

Everything below the --- line is the response.

Status line

The first line after --- is the status line:

HTTP 200

Write it as HTTP followed by a space and a status code between 100 and 599. So that curl -i output can be pasted in unchanged, a version and a reason phrase are also accepted and ignored: HTTP/1.1 404 Not Found and HTTP/2 200 both work. Dartmole always writes the short HTTP 200 form.

Headers

Each following line is a response header, name: value, until the first empty line.

  • Names are case-insensitive. Dartmole writes them in lowercase.
  • The value is everything after the first :, with spaces at either end removed.
  • A name may appear more than once, and each line is sent in the order written. set-cookie needs this.
  • Continuation lines, which start with a space or tab, are not supported and are an error.
  • There are no comments here. A line starting with # is an error, because it is not a header.
  • content-length, content-encoding, transfer-encoding, connection and keep-alive are dropped: Dartmole sends the body uncompressed and works out its length itself. Pasted output often contains content-encoding: gzip although the text is already decompressed, and keeping that header would make the app try to decompress plain text.

Dartmole leaves credential headers such as authorization and set-cookie out of a mock it captures. A mock you write by hand can include them.

Body

Everything after the first empty line is the body, byte for byte, with one exception: exactly one newline at the very end of the file is removed, because editors add one. That newline is \n, or \r\n in a CRLF file.

  • No escaping, no indentation and no quoting. Paste the payload in as it is.
  • To send a body that ends in a newline, leave an empty line after it.
  • CRLF line endings inside the body are kept as written.
  • The body is not checked against content-type. A mock that returns broken JSON is a legitimate way to test how an app copes. The desktop editor warns when a body labelled application/json does not parse, and nothing else.
  • For an empty body, end the file after the header lines. The empty line can be there or not.

Bodies from a file

A body that is not UTF-8 text, such as an image, or one too large to read comfortably next to the rule, goes in a separate file named by bodyFile in the rule:

name: Product image
match:
  path: /images/product/*
bodyFile: product-image.png
---
HTTP 200
content-type: image/png
  • The path is relative to .dartmole/mocks/ and must stay inside it: no absolute paths and no ...
  • The file is sent byte for byte, with nothing removed from the end.
  • When bodyFile is set, the response must end after its headers. A body written below them as well is an error, rather than one of the two winning silently.
  • When Dartmole captures a binary body it names the file after the mock: product-image.png for product-image.mock.
  • Any file named <id>.<something> belongs to that mock. Dartmole deletes it along with the mock, and replaces it whenever the mock is saved from the editor, which writes a text body inline. Give a body file you maintain by hand a name of its own, such as product-image-body.png.

Examples

A JSON response with a delay, to see a loading state:

name: Slow profile
match:
  method: GET
  path: /v1/me
delayMs: 2000
---
HTTP 200
content-type: application/json

{"id": 42, "name": "Ada"}

No body:

match:
  method: DELETE
  path: /v1/sessions/*
---
HTTP 204

An error page that is not JSON:

# What the load balancer returns while a deploy is in progress.
name: Maintenance page
match:
  path: /api/*
---
HTTP 503
content-type: text/html; charset=utf-8
retry-after: 120

<!doctype html>
<h1>Back soon</h1>

Malformed JSON, on purpose:

# The body is cut off, as when a connection drops mid-response.
name: Truncated orders
match:
  path: /v1/orders
---
HTTP 200
content-type: application/json

{"orders": [{"id": 1}, {"id":

One page of a list, while every other page still comes from the backend:

# Page 2 is where the app's infinite scroll broke.
name: Tasks, page 2
match:
  method: GET
  path: /api/v2/tasks
  query:
    page: "2"
---
HTTP 200
content-type: application/json

{"items": [], "page": 2, "last": true}

Two cookies:

match:
  method: POST
  path: /login
---
HTTP 200
set-cookie: session=abc; Path=/; HttpOnly
set-cookie: theme=dark; Path=/

Errors and warnings

A mock that cannot be read is not skipped without a word. The desktop app lists it under "could not be read", with its filename and the first thing that is wrong. The line number counts from the top of the file.

Errors. Dartmole does not load the mock.

Problem Message
No line that is exactly --- no "---" line: the response goes below one
The rule is not valid YAML line N: the rule is not valid YAML: <reason>
The rule is YAML but not a mapping the rule must be keys and values, like "name: …"
delayMs is not a whole number ≥ 0 line N: "delayMs" must be a whole number of milliseconds
match is not a mapping line N: "match" must be keys and values, like "path: /v1/users"
match.query is not a mapping line N: "query" must be names and values, like "page: \"1\""
A query parameter with no value line N: query "<name>" needs a value, or "*" for any value
A query value that is not text line N: query "<name>" must be text, or a list of text
name, a match field or bodyFile is not text line N: "<key>" must be text
Nothing after --- line N: expected "HTTP <status>" after "---"
Bad status line line N: expected "HTTP <status>", got "<line>"
Status outside 100–599 line N: status <code> is not between 100 and 599
A header line without : line N: expected "name: value", got "<line>"
A continuation line line N: header lines cannot start with a space
bodyFile missing bodyFile "<path>" does not exist
bodyFile outside the folder bodyFile must be inside .dartmole/mocks/
bodyFile and a written body line N: remove the body or the bodyFile, not both
The file is not UTF-8 not UTF-8 text; put binary bodies in a bodyFile

A number or true where text is expected is read as that text, so name: 404 is the name 404.

Warnings. The mock loads and works. The desktop app does not show them yet, apart from the JSON warning, which the editor shows while you type.

Problem Message
An unknown key line N: unknown key "<key>" is ignored
An empty match matches every request
A dropped header line N: "<name>" is dropped; Dartmole sets it itself
A JSON body that does not parse body is labelled JSON but does not parse

A rule that begins with a --- line, as Jekyll-style front matter does, has no rule of its own. It surfaces as a bad status line (got "name: …"), so that message should also say "the file starts with YAML, not with ---".

Compatibility

  • The format only gains things. New keys are optional, and a mock that loads today will load in every later version.
  • There is no version: key. If a change ever cannot be added this way, it has to be recognisable from the file itself, for example by a new extension.