Skip to main content

Writing OpenAPI Specs

A step plugin defines all its steps, routes, and parameters in an OpenAPI specification file. The spec file represents a contract between Cycle and the step plugin. When Cycle consumes the step plugin as a plugin, it reads the OpenAPI spec to get the full list of its steps. When editing CycleScript, editors include the step plugin's steps in features like syntax highlighting and automatic completion. When executing feature files, the Cycle engine connects CycleScript step text to the step plugin endpoints that implement them. The OpenAPI spec must adhere to both the syntactic rules of OpenAPI and the required conventions of Cycle.

Supported OpenAPI versions

Cycle supports OpenAPI versions 3.x.x.

Writing OpenAPI spec files

Step plugin developers should be familiar with OpenAPI. If you are new to OpenAPI, please review OpenAPI docs from the OpenAPI Initiative or Swagger to learn how to write spec files in full. The sections below will show example snippets.

If you are using VS Code for development, you can install the OpenAPI (Swagger) Editor extension for advanced OpenAPI spec file editing features.

Setting plugin information

The top of the OpenAPI spec file should set important plugin information, such as:

  • the OpenAPI version
  • the plugin's name
  • the plugin's description
  • the plugin's license (if any)
  • the plugin's version (using Semantic Versioning)
  • the plugin's unique Cycle namespace (to avoid collisions with other step plugins)

Below is an example snippet of this information:

openapi: 3.0.3

info:
title: Selenium WebDriver Step Plugin
description: |
This plugin provides steps for browser interactions.
It uses Selenium WebDriver to interact with browsers.
It can manage multiple WebDriver sessions, either locally or remotely.
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
version: 1.0.0
x-cycle-namespace: webdriver
# ...

Writing step definitions

Each step that a plugin provides needs a unique path entry ("endpoint") in the OpenAPI spec. A step plugin may provide any number of steps, from a small handful to several dozen.

Consider the following CycleScript step:

When I navigate to "https://cyclelabs.io/" in web browser

This step navigates the browser to the given URL address. Its OpenAPI path definition could look like this:

tags:
# ...
- name: Navigation
description: Handle browser navigation.

paths:
# ...

'/sessions/{WEBDRIVER_SESSION_ID}/interaction/navigate-to-url':
post:
operationId: navigateToUrl
tags: [Navigation]
description: |
Navigates to the specified URL (website address) in the web browser.
The full URL, including the protocol identifier, must be provided (e.g., "http://www.mywebsite.com").
Default timeout of 120 seconds unless otherwise specified using "within".
x-cycle-steps:
- I navigate to {url} in web browser
- I navigate to {url} in web browser within {timeoutValue} {timeoutUnit}
parameters:
- in: path
name: WEBDRIVER_SESSION_ID
required: true
schema:
type: string
format: uuid
- in: query
name: timeoutValue
required: false
schema:
type: integer
minimum: 0
default: 120
- in: query
name: timeoutUnit
required: false
schema:
type: string
enum:
- seconds
- ms
default: seconds
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
url:
type: string
required:
- url
responses:
'200':
description: The outcome of the step.
content:
application/json:
schema:
$ref: '#/components/schemas/StepResponse'

There's a lot to this definition. Let's unpack it piece by piece.

Matching step text

The resource path for this step is /sessions/{WEBDRIVER_SESSION_ID}/interaction/navigate-to-url. Step plugins can adopt any naming convention for paths, but it is recommended to follow common REST API naming conventions. To perform navigation, the step needs the browser that the test is using. Each part of the resource path has an important meaning:

  • sessions refers to WebDriver session resources that the step plugin manages.
  • {WEBDRIVER_SESSION_ID} is a path parameter input for the WebDriver session ID.
    • The next section will cover inputs.
  • interaction denotes that this step is an interaction rather than a verification.
    • Including interaction or verification in the path is conventional.
  • navigate-to-url refers to the step as the target resource.

The properties under the resource path are significant:

  • The post: directive means that this endpoint should be called with the HTTP POST method. Step plugins can define steps to use any HTTP method, but in most cases, steps should use POST because they perform actions.

  • The operationId: directive sets a unique identifier for the step. This step's ID is navigateToUrl. Providing a unique operation ID is required.

  • The tags: and description: directives are optional but recommended. Tags group steps together, and descriptions provide extra information about the step that will appear in a step's help text while editing. These fields may also affect OpenAPI code generation, depending on the tools and languages used.

  • The x-cycle-steps: directive defines the list of CycleScript step texts for this step. The Cycle Engine will match CycleScript steps (like I navigate to {url} in web browser) to step plugin steps based on the lines provided here. Multiple CycleScript steps can be matched to one step plugin definition. Step parameters are placed between curly braces. Each step parameter should have a parameter defined in the OpenAPI path definition, which defines its type. The step text under x-cycle-steps does not need to include double quotes for string values.

So, in summary, the step:

When I navigate to "https://cyclelabs.io/" in web browser

Matches to the endpoint:

POST /sessions/{WEBDRIVER_SESSION_ID}/interaction/navigate-to-url

Its ID is navigateToUrl, and it is part of the Navigation group. Its single parameter is named url.

Matching step inputs

The parameters: and requestBody: directives define all the inputs for the step. All OpenAPI parameter types (path, query, header, and cookie) are supported. A request body is optional, but if it is provided, then it must be a JSON object, and the inputs should be top-level properties within the object.

Across the parameters and request body, this step has 4 inputs:

  1. url
  2. timeoutUnit
  3. timeoutValue
  4. WEBDRIVER_SESSION_ID

url is a string value in the request body. url is also the name of a parameter in the CycleScript step: I navigate to {url} in web browser. When calling this step, Cycle will pass the CycleScript step's url parameter into the request body.

timeoutUnit and timeoutValue are optional query parameters. Again, these values come from the CycleScript step. There are two CycleScript versions of the step: one with these timeout values, and one without them. Any optional parameters must have default values.

WEBDRIVER_SESSION_ID is a path parameter. However, its value does not come from the CycleScript step. Whenever an input value is not a CycleScript step parameter, Cycle treats it as a Cycle variable or property. So, when Cycle calls this step, it should pass in the current Cycle variable value for WEBDRIVER_SESSION_ID. If no Cycle variable or property exists for the given name, then Cycle raises an error.

Cycle also uses these input definitions to check the types of step parameters. For example, timeoutValue must be a non-negative integer, so a step like When I navigate to "https://cyclelabs.io" in web browser within -10 seconds should yield an error.

In summary, if Cycle already has a variable WEBDRIVER_SESSION_ID with the value d56234a2-1fca-48a7-b445-e07b0ca65c9e, then to call the step:

When I navigate to "https://cyclelabs.io/" in web browser within 10 seconds

Then Cycle sends an HTTP POST request to the step plugin at:

/sessions/d56234a2-1fca-48a7-b445-e07b0ca65c9e/interaction/navigate-to-url?timeoutValue=10&timeoutUnit=seconds

With the request body:

{
"url": "https://cyclelabs.io/"
}

General recommendations:

  • Resource IDs should be passed as path parameters
  • Timeout values should be passed as query parameters
  • Almost all other inputs should be passed in the request body

Warning: The requestBody schema must be defined in-line. OpenAPI allows for $ref entries to components defined under the components: directive, but Cycle does not support component references as of version 2.20. Cycle will add support for component references in a future release.

Warning: Cycle does not support OpenAPI directives to combine schemas (oneOf, anyOf, allOf, not).

Matching Cycle properties

Certain steps may need special properties from Cycle that are not provided as CycleScript step parameters or as regular Cycle variables. For example, WebDriver steps to start a new WebDriver session need the output directory for screenshots, paths for WebDriver executables on the local machine, and possibly saved user credentials. These inputs are project settings and execution settings. All of them must come from Cycle.

If Cycle cannot match an input as a CycleScript step parameter or as a regular Cycle variable, it will attempt to match it as a Cycle property. The Cycle engine has a pre-defined set of Cycle properties that it can pass into a step plugin step. These Cycle properties all start with the prefix _CYCLE_. For example, _CYCLE_DEFAULT_OUTPUT_DIR is the absolute path to the current Cycle project's default output directory (for screenshots). The Engine will match these Cycle properties by name in the OpenAPI path definitions. Typically, steps should define inputs for Cycle properties in request bodies.

The full list of supported Cycle properties is documented on the Using Cycle Properties page.

Returning step responses

A step is "successful" if:

  • The Cycle engine provides all the proper inputs.
  • The step plugin completes the step's operation.
  • The step plugin returns an accurate response (PASS or FAIL).

The step plugin should return a successful HTTP status code whenever a step is successfully called and completed - even if that step yields a FAIL status with an error message. Each step defines its own success status code value, but most steps should use 200.

The step plugin should yield a failure HTTP status code if inputs are incorrect (400s) or the step plugin itself has an internal server error (500s). A failure code should indicate a deeper issue in Cycle rather than a failure test result.

In addition to the status code, every step must return the same type of response object with the following fields:

  1. status: "pass" or "fail"
  2. message: a string for any log messages from the step
  3. errorMessage: a string for any error/failure messages from the step
  4. variables: an array of variables with fields for name and value

Only status is required. All others are optional.

Variables are the return values for a step. They are the main way to manage state between Cycle and the step plugin. The variables will be added to Cycle's environment of variables so that other steps can use them. For example, Web steps to initialize a WebDriver session could return WEBDRIVER_SESSION_ID so that other steps (like the aforementioned navigation step) can use it. The Engine should overwrite any previous variable by name with the new value returned by a step.

The OpenAPI spec for the response body is below:

components:
schemas:
# ...

StepResponse:
type: object
properties:
status:
$ref: '#/components/schemas/ExecutionStatus'
variables:
type: array
items:
$ref: '#/components/schemas/Variable'
message:
description: A standard message for step output.
type: string
errorMessage:
description: An error message for a bad request.
type: string
required:
- status

ExecutionStatus:
description: |
Execution Status:
* `pass` - The step executed successfully
* `fail` - The step failed due to a variety of reasons which may include but not limited to failed expectations, invalid preconditions, timeouts, etc.
type: string
enum:
- pass
- fail

Variable:
description: A variable to return from the step.
type: object
properties:
name:
type: string
value:
type: string
required:
- name
- value

Including step examples

Cycle's Step Assistant provides examples of CycleScript step usage. Step plugins do not provide full-line examples of step texts. Instead, they provide examples for all step parameter values using OpenAPI's standard example fields. Cycle will use the example values to concatenate example step values when parsing the OpenAPI spec.

For example, the step used in previous sections has the following CycleScript texts:

I navigate to {url} in web browser
I navigate to {url} in web browser within {timeoutValue} {timeoutUnit}

The url, timeoutValue, and timeoutUnit step parameters should all be given example values like this:

  '/sessions/{WEBDRIVER_SESSION_ID}/interaction/navigate-to-url':
post:
# ...
x-cycle-steps:
- I navigate to {url} in web browser
- I navigate to {url} in web browser within {timeoutValue} {timeoutUnit}
# ...
parameters:
# ...
- in: query
name: timeoutValue
required: false
schema:
type: integer
minimum: 0
default: 120
example: 10
- in: query
name: timeoutUnit
required: false
schema:
type: string
enum:
- seconds
- ms
default: seconds
example: seconds
# ...
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
url:
type: string
example: https://cyclelabs.io/
required:
- url
# ...

Every input for a CycleScript step parameter should be given an example value. Other inputs may also have example values, but they will not be used for generating the step examples. The example above should produce the following step examples:

I navigate to "https://cyclelabs.io/" in web browser
I navigate to "https://cyclelabs.io/" in web browser within 10 seconds

The example above shows one example value per input. OpenAPI also supports providing multiple examples. When multiple examples are provided, Cycle should generate step examples that use each step parameter example. Examples should each be applied round-robin instead of by cross-product to limit the number of generated step examples. For instance, if the following example values are provided:

url = ["https://cyclelabs.io/"]
timeoutValue = [10, 10000]
timeoutUnit = [seconds, ms]

Then, the following step examples should be generated:

I navigate to "https://cyclelabs.io/" in web browser
I navigate to "https://cyclelabs.io/" in web browser within 10 seconds
I navigate to "https://cyclelabs.io/" in web browser within 10000 ms