Skip to main content

Writing a Spec

Manual tests, known as specs, are written in Markdown (CommonMark, with GitHub-style tables, task lists, autolinks and strikethrough), enhanced with the Liquid template language. This page covers the spec format itself, which is the same whether your project is Standalone or Connected.

tip

See Creating & Editing Specs for how to write, preview, and save a spec, including the Standalone editor's toolbar.

Test Syntax​

A spec is a single Markdown document consisting of:

  • A Front Matter header block
  • A test spec name
  • At least one test case
  • Optional test steps
---
testspace:
---
# Test 1
## Case 1
- do this
- do that
## Case 2
..
tip

See the Markdown Cheat Sheet for helpful hints on leveraging markdown to capture test instructions.

Front Matter​

Every spec starts with a metadata block called front matter: YAML between two triple-dash (---) lines, containing the testspace: identifier. It also holds the spec's custom variables and its automated fixtures.

---
testspace:
---
  • Connected: the front matter is the first thing in the .md file, and identifies the file as a Testspace spec.
  • Standalone: the editor adds the front matter when you submit, so you don't type it. Custom variables and fixtures go in the editor's Variables box, or through Manage (see Creating & Editing Specs). A Standalone spec can't be saved without the identifier ("Spec must include the testspace front matter identifier").

Disable Spec​

To disable a spec, set the testspace value to false:

---
testspace: false
---

A disabled spec can't be selected for a new session or run ("Disabled - running is disallowed."). It stays in the Space, so its existing status, and any failures being tracked for it, are kept.

Remove Spec​

In a Connected project, to remove a spec without deleting its file, comment out (#) the testspace identifier:

---
#testspace:
---

Testspace then treats the spec as deleted: its status is removed from the next Result, and its tracked failures are retired at the next session (Failure Tracking). In a Standalone project, use Delete on the spec instead (Creating & Editing Specs).

Spec​

Each spec requires a spec name written as a Markdown <H1>, following the front matter. Any text after the spec name is the spec's description. A Standalone spec with no <H1> can't be saved ("Spec content can't be empty -- give it a title.").

---
testspace:
---

# My Spec

Spec description text

Case​

A spec requires at least one test case. A case name is a Markdown <H2> heading. Any text following the heading is the case's description.

# My Spec

## My Case
Case description text

## My Other Case

Case names must be unique within a spec. A Standalone spec with two cases of the same name can't be saved ("Duplicate case name is not allowed"); a Connected spec with duplicates fails to parse and can't be selected for a session.

info

Testspace matches a case to its history by name. Renaming or removing a case loses that case's history, and its tracked failures are retired at the next session that includes the spec (Failure Tracking).

Step​

Steps are optional instructions to be executed for a test case. Steps are written using a markdown (ordered or unordered) list inside a test case.

## My Case

* My step 1 instructions
* My step 2 instructions

Steps can also exist outside a test case, in which case they are meant to represent a repeated context that is applicable to each test case in the spec.

Checklist Steps​

A step written as a GitHub-style task-list item becomes a checkbox a tester can check off individually while executing the case:

## My Case

- [ ] My step 1 instructions
- [ ] My step 2 instructions

Fixtures​

Test case Fixtures implement steps/conditions that are required for executing each case within the spec. They are defined using the same markdown syntax but named in a special way.

:::info This is a different concept from **Automated Fixtures** (serverless automation that runs before/after a spec) — see [Automated Fixtures](automated-fixtures). :::
info

A fixture can be defined anywhere in the file, but only one time.

Setup is a fixture, defined as a set of instructions, to be presented before each case during execution.

A Setup fixture is defined using the ## [setup] syntax:

## [setup]
This is my setup fixture:

- First do this
- Next do that

The Setup fixture can be defined anywhere in the file, but only one time.

Teardown is a fixture, defined as a set of instructions, to be presented after each case during execution.

A Teardown fixture is defined using the ## [teardown]:

## [teardown]
This is my teardown fixture:

- Now do this
- And do that

Template Language​

Testspace supports the template language called Liquid. Specs are handled as templates, meaning they get processed before being rendered. A spec can use variables, include files (i.e. subroutines) with parameters, and conditional logic.

tip

In a Standalone Project, the editor's toolbar inserts most of this for you: Include, Variable and If, plus the presentational tags. See Creating & Editing Specs.

Variables​

There are two types of variables supported by Testspace: custom and global.

Custom​

User-defined "custom" variables are defined in the front matter block and can be referenced throughout the spec. In a Standalone Project, add them with Manage or type them in the editor's Variables box. id, filename and filepath are reserved names.

---
testspace:
var1: Hey there
---

When referencing a variable, the following syntax is required: {{ spec.VAR }}.

## My Case

* My first step uses this {{ spec.var1 }}
* My Other Step
tip

Scalar custom variable values are always stored and interpolated as strings, even something that looks numeric — count: 5 becomes the string "5", not an integer. Comparing with ==/>/< against an unquoted numeric literal silently never matches: {% if spec.count == 2 %} is never true, since spec.count is the string "2", not the number 2. Quote the literal instead: {% if spec.count == "2" %}.

A range, e.g. {% for i in (1..spec.count) %}, isn't affected — Liquid converts both bounds to integers regardless of the input's type.

Global​

The following global variables are supported: {{ variable }}.

VariableDescription
project.nameThe name of the Testspace project
project.idThe assigned project id
space.nameThe name of the space
space.idThe assigned space id
spec.filenameThe spec's file name, excluding the folder (e.g. login-steps.md)
spec.filepathThe spec's folder (e.g. specs in Connected; . for a Standalone spec with no folder)
spec.idThe spec's id (in an automated fixture's context, spec.id is the id of the spec's run instead)
repo.branchThe branch name (same as the space.name). Connected Projects only — always empty for Standalone.
repo.urlThe branch URL (e.g. "https://github.com/org/repo"). Connected Projects only — always empty for Standalone.

The following global variables take their values only when the spec is run in a session. In the editor's Preview, session.* shows as a placeholder:

VariableDescription
user.nameThe name of the user running the spec
user.idThe id of the user running the spec
session.nameThe session's name
session.idThe session's id

The following is a simple example of referencing a global variable:

## My Case
Enter the session name: {{ session.name }} in the comments below

* Check this
* Check that

Includes​

The include tag pulls in content from another reusable file, referenced by name:

{% include file.md %}

Both custom and global variables are referenceable within an include file using the {{ }} syntax. Includes can reference other includes.

info

Avoid circular references — an include that, directly or indirectly, ends up including itself will cause the spec to fail to parse.

# My Spec

* Step One

{% include morestuff.md %}
{% include sub-folder/otherstuff.md %}

Where an include's content actually lives, and how you create and manage one, depends on your project type — see Includes.

Parameters​

Parameters can also be passed to the included file using var="string":

{% include morestuff.md domain="testspace.com" %}

When referencing a passed "parameter" via the include tag, the following syntax is required: {{ include.VAR }}.

Logic​

Logic and control flow for specs is accomplished using Liquid tags. The curly-brace percentage delimiters {% %} and the text that they surround, does not produce any visible output when the spec is rendered. The tags enable assigning variables and creating conditions and loops without showing any of the Liquid logic on the page.

if-else (assign sets a variable for this example — see Variables in the Liquid Cheat Sheet below)

{% assign OS = 'Windows' %}
{% if OS == 'Windows' %}
- check for Windows stuff.
{% elsif OS == 'Linux' %}
- check for Linux stuff
{% else %}
- check for Mac stuff
{% endif %}

for-loop

{% for i in (1..5) %}
{{ i | plus: 100}}
{% endfor %}
tip

For more information on logic and control flow using Liquid refer here. See Control and Loops in the Liquid Cheat Sheet below for more constructs — unless, case/when, loop modifiers, and forloop.

Presentational Tags​

A small set of Testspace-specific Liquid tags render as formatted UI elements — callouts, tabs, and a collapsible section — rather than plain markdown. Available on both Connected and Standalone Projects (they don't render in Desktop Preview). Each compiles to a fixed, server-rendered fragment — same trust model as {% include %} — nothing a spec author writes here gets executed as code.

{% note %}
Run this case after `Cart Setup`. Requires a seeded promo code (`SAVE20`).
{% endnote %}

**Steps**
- [ ] Add any item to the cart
- [ ] Navigate to **Checkout**
- [ ] Enter promo code `SAVE20` in the "Promo Code" field
- [ ] Click **Apply**

{% warning %}
If the field shows "Invalid Code," confirm `SAVE20` hasn't expired — known flaky test data, not a product bug.
{% endwarning %}

{% tabs %}
{% tab "Web" %}
- [ ] Discount line appears above "Order Total"
{% endtab %}
{% tab "iOS" %}
- [ ] Discount line appears in the order summary sheet
{% endtab %}
{% endtabs %}

{% collapse "Expected server response (reference)" %}
```json
{ "discount_code": "SAVE20", "discount_type": "percent", "discount_value": 20 }
```
{% endcollapse %}

In a Standalone Project, each has a button in the spec and include editor's toolbar (Tabs, Note, Warning, Collapse) that inserts a starting skeleton for it.

The four tags, at a glance​

iNote

{% note %} … {% endnote %}

Context a tester needs before starting — preconditions, environment assumptions, seed data.

!Warning

{% warning %} … {% endwarning %}

Flags a known gotcha inline, at the exact step it applies to — flaky data, environment quirks, "don't file a bug for this."

⇥Tabs

{% tabs %} {% tab "…" %}

One case, several platform- or role-specific step lists — instead of duplicating the whole spec, or one giant if/else wall.

▾Collapse

{% collapse "…" %} … {% endcollapse %}

Reference material worth keeping next to the steps — a payload, a query, a longer explanation — without pushing past it to keep reading.

tip

Markdown inside these tags — including task-list steps (- [ ] ...) — renders normally, same as anywhere else in a spec.


Examples​

The following examples show more complete specs combining variables, includes, parameters, and logic.

Example using an include​

include: inc.md
## Case 2
My description here
- Check this subdomain: {{ spec.subdomain }}
- Click here: https://{{ spec.subdomain }}.{{ include.domain }}

Example of a spec including an include and passing a parameter:

spec: myspec.md
---
testspace: true
title: Vars and Include
subdomain: s2
---

# {{ spec.title }}
Some description

## Case 1
- Do this
- Do that

{% include inc.md domain="testspace.com" %}

Renders to:

Rendered output

Vars and Include

Some description

Case 1

  • Do this
  • Do that

Case 2 My description here

Example using data driven testing​

Using front matter data to drive testing:

spec: os-systems-test.md
---
testspace:
title: OS Systems
matrix: # test different OS systems
- name: Windows
timeout: 27 seconds
reqs: "[Windows details](https://staging7.newco.com/windows)"
- name: Linux
timeout: 14 seconds
reqs: "[Linux details](https://staging7.newco.com/linus)"
---

# {{ spec.title }}
Matrix being tested.

Name | Timeout | Info
-----| --------| -----
{%- for os in spec.matrix %}
{{ os.name }} | {{ os.timeout}} | {{os.reqs -}}
{% endfor %}

{% for os in spec.matrix %}
## Test {{ os.name }}
* check for correct timeout: {{ os.timeout}}
* check on requirements: {{ os.reqs }}
{% endfor %}

Renders to:

Rendered output

OS Systems

Matrix being tested.

NameTimeoutInfo
Windows27 secondsWindows details
Linux14 secondsLinux details

Test Windows

  • check for correct timeout: 27 seconds
  • check on requirements: Windows details

Test Linux

  • check for correct timeout: 14 seconds
  • check on requirements: Linux details

Example combining presentational tags with data-driven content​

Presentational Tags aren't a separate, static feature — they compose with everything else on this page. Here, a {% collapse %} and forloop.index (see Loops in the Cheat Sheet below) are generated fresh for each row of front matter data:

spec: os-systems-test.md
---
testspace:
title: OS Systems
matrix:
- name: Windows
timeout: 27 seconds
setup: "Provision a Windows 11 VM from the shared pool."
- name: Linux
timeout: 14 seconds
setup: "Provision an Ubuntu 22.04 container."
---

# {{ spec.title }}

{% for os in spec.matrix %}
## Test {{ forloop.index }}: {{ os.name }}

{% collapse "Setup instructions" %}
{{ os.setup }}
{% endcollapse %}

* check for correct timeout: {{ os.timeout }}
{% endfor %}

Markdown Cheat Sheet​

The following Markdown syntax is supported.

Lists​

Lists are used within steps and fixtures, but can also be nested.

Bullets

  • First Step
    • sub-step 1
    • sub-step 2
  • Second Step
* First Step
* sub-step 1
* sub-step 2
* Second Step

Numbers

  1. First Step
  2. Second Step
1. First Step
2. Second Step

Todos

A step written as a task-list item becomes an individually checkable box during execution:

  • First Step
  • Second Step
- [ ] First Step
- [ ] Second Step
tip

See Checklist Steps for more on task-list steps.

Liquid Cheat Sheet​

Quick reference for Liquid mechanics not already covered above. For variables, includes, and if/for logic, see the Template Language sections above.

Variables​

Tags for creating variables mid-template, as opposed to reading ones already supplied — see Variables above for those. Every example below is self-contained; paste it as-is into a spec to see it work.

assign — creates or overwrites a variable with a computed value:

{% assign timeout = 5 | times: 2 %}
Timeout is {{ timeout }} seconds

capture — grabs a block's rendered output into a variable instead of displaying it, useful for building up reusable text:

{% assign name = 'Alex' %}
{% capture greeting %}Hello, {{ name }}!{% endcapture %}
{{ greeting }}

increment / decrement — each maintains its own independent counter, separate from any variable of the same name created with assign. increment starts at 0 and outputs the value before incrementing; decrement starts at -1 and outputs the value after decrementing:

{% increment counter %}
{% increment counter %}
{% decrement counter %}

Outputs 0, then 1, then -1.