Skip to main content

Writing a Spec

Manual tests, known as test specs, are written using Markdown syntax, enhanced with the Liquid templating language. This page covers the spec format itself — the same format regardless of whether your project is Standalone or Connected.

tip

See Creating & Editing Specs for how to actually write, preview, and save a spec.

Test Syntax​

A manual test 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​

To uniquely identify a markdown file as a Testspace spec file, a metadata block is required. This metadata is called front matter, denoted by the triple dashes (---) at the start and end of the block. It must be the first thing in the file, YAML formatted, containing the testspace: identifier.

---
testspace:
---

Disable Spec​

To disable a test spec set the testspace token to false:

---
testspace: false
---

This setting prevents the spec from being selected on new test sessions. The existing status for the test spec will be maintained.

Remove Spec​

To remove a test spec, without deleting it, comment out (#) the testspace token:

---
#testspace:
---

The existing status will be removed on the subsequent result.

Spec​

Each spec also is required to contain a spec name written in the <H1> markdown syntax, following the front matter block. Any optional text after the spec name is the description of the spec.

---
testspace:
---

# My Spec

Spec description text

Case​

A spec requires at least one test case. A case name is denoted by a markdown <H2> heading. Any optional text following the heading is the test case description.

# My Spec

## My Case
Case description text

## My Other Case

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. Test specs are handled as template files, meaning they get preprocessed before being rendered. This functionality enables a test spec file to use variables, include files (i.e. subroutines) along with passing parameters, and even the ability to implement conditional logic.

Variables​

There are two types of variables supported by Testspace: custom and global. Testspace traverses your spec files and processes variables.

Custom​

User-defined "custom" variables are defined in the front matter block and are referenceable throughout the spec.

---
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 test spec file name, excluding the path
spec.filepathThe test spec file path
spec.idThe assigned test spec id
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 are instantiated and only valid in the context of an open test session:

VariableDescription
spec.run_idThe assigned run id
user.nameThe name of the user executing the spec (if assigned)
user.idThe user's assigned id ( if assigned)
session.nameThe session's name entered via the user
session.idThe session assigned 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 test 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 test 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 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. 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 %}

Each has a button in the spec/include editor's toolbar that inserts a starting skeleton for it — no need to type the tags by hand.

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 test spec including and passing parameters:

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.