Step Definitions
Include cucumber.hpp (or cwt-cucumber/cucumber.hpp, depending on your installation) to define and implement your steps.
Implementing Steps & Value Access
Use the provided macros to define your steps:
GIVEN(function_name, "your step goes here")WHEN(function_name, "your step goes here")THEN(function_name, "your step goes here")Alternatively, use
STEP(function_name, "your step goes here")if you don’t want to specify the step type explicitly.
Example:
#include "cucumber.hpp"
#include "box.hpp"
GIVEN(init_box, "An empty box")
{
const box& my_box = cuke::context<box>();
cuke::equal(my_box.items_count(), 0);
}
WHEN(add_item, "I place {int} x {string} in it")
{
const std::size_t count = CUKE_ARG(1); // {int}
const std::string item = CUKE_ARG(2); // {string}
cuke::context<box>().add_items(item, count);
}
THEN(check_box_size, "The box contains {int} items")
{
const int items_count = CUKE_ARG(1); // {int}
const box& my_box = cuke::context<box>();
cuke::equal(my_box.items_count(), items_count);
}
Accessing Values
Use CUKE_ARG(index) to access a value from a step by its index (starting at 1, left to right).
The values are automatically converted according to their type.
Example:
STEP(your_function, "A {string}, an {int} and a {float}")
{
std::string str = CUKE_ARG(1); // {string}
int i = CUKE_ARG(2); // {int}
float f = CUKE_ARG(3); // {float}
// ...
}
Note
CUKE_ARG relies on implicit conversion operators; declaring variables with auto will not work.
Supported Cucumber expressions include:
Integer values:
{byte},{short},{int},{long}Floating point values:
{float},{double}Strings (quoted):
{string}Words (unquoted):
{word}→ read asstd::stringAnonymous placeholders:
{}→ read asstd::stringCustom Parameter Types:
{your_type}→ see Custom Parameter Types for examples
Asserts
Use the following functions to verify expected values:
cuke::equal(val1, val2)cuke::not_equal(val1, val2)cuke::greater(val1, val2)cuke::greater_or_equal(val1, val2)cuke::less(val1, val2)cuke::less_or_equal(val1, val2)cuke::is_true(val)cuke::is_false(val)
Each assert function optionally accepts a custom error message:
THEN(check_box_size, "The box contains {int} items")
{
const int items_count = CUKE_ARG(1);
const box& my_box = cuke::context<box>();
cuke::equal(my_box.items_count(), items_count, "Items count is incorrect!");
}
For more details, see Asserts.
Scenario Context (cuke::context)
The scenario context stores objects for the duration of a scenario. Objects are automatically destroyed at the end of the scenario. One instance per type can be stored per scenario.
Example:
// Forward constructor arguments
cuke::context<some_object>(1, 2, 3);
// Default initialization
cuke::context<some_object>();
Accessing stored objects:
WHEN(add_item, "I place {int} x {string} in it")
{
const std::size_t count = CUKE_ARG(1); // {int}
const std::string item = CUKE_ARG(2); // {string}
cuke::context<box>().add_items(item, count);
}
THEN(check_box_size, "The box contains {int} items")
{
const int items_count = CUKE_ARG(1); // {int}
const box& my_box = cuke::context<box>();
cuke::equal(my_box.items_count(), items_count);
}
Word Alternations
CWT-Cucumber supports word alternations in step definitions using parentheses () and slashes /. This allows a single step definition to match multiple word variations in your feature files.
Example:
THEN(alternative_words, "{int} item(s) is/are {string}")
{
// Implementation here
}
With this step definition, you can write scenarios using either word form:
Scenario: Alternative Words
Given An empty box
When I place 1 x "banana" in it
Then 1 item is "banana"
And I place 1 x "banana" in it
Then 2 items are "banana"
Hooks
Hooks allow you to execute code before or after scenarios or steps. They are easy to implement and can be used for setup, teardown, logging, or conditional scenario handling. Multiple hooks of the same type are allowed; they are executed in the order they are defined.
Basic Hooks
CWT-Cucumber provides several types of hooks:
BEFORE(before)
{
// Runs before every scenario
}
AFTER(after)
{
// Runs after every scenario
}
BEFORE_STEP(before_step)
{
// Runs before every step
}
AFTER_STEP(after_step)
{
// Runs after every step
}
BEFORE_ALL(before_all)
{
// Runs once at program start
}
AFTER_ALL(after_all)
{
// Runs once at program end
}
Tagged Hooks
Hooks can be limited to specific scenarios using tag expressions.
BEFORE_T(name, "tag expression")– runs before a scenario that matches the tag expressionAFTER_T(name, "tag expression")– runs after a scenario that matches the tag expression
A tagged hook is executed only when a scenario matches the specified tag expression. You can use any logical combination of tags.
Example:
AFTER_T(dispatch_box, "@ship or @important")
{
std::cout << "The box is shipped!" << std::endl;
}
@ship
Scenario: We want to ship cucumbers
Given An empty box
When I place 1 x "cucumber" in it
Then The box contains 1 item
@important
Scenario: Important items must be shipped immediately
Given An empty box
When I place 2 x "important items" in it
Then The box contains 2 items
Skipping & Ignoring Scenarios
You can skip or ignore scenarios from within hooks:
cuke::skip_scenario()– marks a scenario as skipped (reported as skipped)cuke::ignore_scenario()– marks a scenario as ignored (not reported)
Example:
BEFORE_T(skip, "@skip")
{
// Skip this scenario
cuke::skip_scenario();
}
BEFORE_T(ignore, "@ignore")
{
// Ignore this scenario
cuke::ignore_scenario();
}
Note
The AFTER_ALL hook is always executed.
Hooks AFTER and AFTER_T are not executed for skipped or ignored scenarios.
Manual Failures
You can manually fail a scenario or a step from within hooks using:
cuke::fail_scenario()
Fails the entire scenario, optionally with a custom message:
BEFORE(before)
{
// Fail scenario due to a setup error
cuke::fail_scenario();
cuke::fail_scenario("Custom error message");
}
When called in a before hook: - All steps are skipped - The message is displayed with each skipped step
When called in an after hook: - The scenario is marked as failed even if all steps passed
Note
CWT-Cucumber reports the message under the steps; the scenario itself does not display a separate error.
cuke::fail_step()
Fails the current step, optionally with a custom message:
BEFORE_STEP(before_step)
{
// Fail the step due to an error condition
cuke::fail_step();
cuke::fail_step("Step failed due to invalid setup");
}
Intended for use in before step hooks
Has no effect in after step hooks
Marks the step as failed and shows the error message in output
Doc Strings
Doc Strings are multi-line text blocks attached to a step, often used for JSON, configuration, or long messages.
You can access a doc string using the macro CUKE_DOC_STRING(). There are two options:
As a single string containing the entire content
As a
std::vector<std::string>containing each line
Example:
WHEN(doc_string, "There is a doc string:")
{
// Get the entire doc string as a single string
const std::string& str = CUKE_DOC_STRING();
// Or get each line separately in a vector
const std::vector<std::string>& lines = CUKE_DOC_STRING();
}
Datatables
Datatables provide tabular input for a step.
You can access a table attached to a step using the macro CUKE_TABLE():
As a const reference:
const cuke::table& t = CUKE_TABLE();As a copy:
cuke::table t = CUKE_TABLE();
The table contains cuke::value objects. You can access values with operator[] and convert them using as<>() or to_string():
const cuke::table& t = CUKE_TABLE();
std::string s = t[0][0].to_string();
int i = t[0][1].as<int>();
There are three main ways to access table data:
Raw Access
Use raw tables when there is no header row or descriptive column names:
When I add all items with the raw function:
| apple | 2 |
| strawberry | 3 |
| banana | 5 |
You can iterate over the table using raw():
WHEN(add_table_raw, "I add all items with the raw function:")
{
const cuke::table& t = CUKE_TABLE();
for (const auto& row : t.raw())
{
cuke::context<box>().add_items(row[0].to_string(), row[1].as<long>());
}
}
Hashes
Use hashes tables when your table has a header row to make it more descriptive:
When I add all items with the hashes function:
| ITEM | QUANTITY |
| apple | 3 |
| banana | 6 |
You can iterate over the table using hashes() and access elements by header name:
WHEN(add_table_hashes, "I add all items with the hashes function:")
{
const cuke::table& t = CUKE_TABLE();
for (const auto& row : t.hashes())
{
cuke::context<box>().add_items(row["ITEM"].to_string(), row["QUANTITY"].as<long>());
}
}
Key/Value Pairs (Rows Hash)
The rows hash provides a convenient way to access table data using key/value pairs. The first column acts as the key, and the second column holds the value:
When I add the following item with the rows_hash function:
| ITEM | really good apples |
| QUANTITY | 3 |
You can convert the table into a hash map using rows_hash():
WHEN(add_table_rows_hash, "I add the following item with the rows_hash function:")
{
const cuke::table& t = CUKE_TABLE();
// cuke::table::pair is an alias for std::unordered_map
cuke::table::pair hash_rows = t.rows_hash();
cuke::context<box>().add_items(hash_rows["ITEM"].to_string(), hash_rows["QUANTITY"].as<long>());
}
Note
Always cast cuke::value objects to the correct type using as<>() or to_string().
This ensures proper handling of integers, strings, or floating-point values.
Custom Parameter Types
CWT-Cucumber allows defining custom parameter types to make step definitions more readable and expressive. A custom parameter type converts a matched value from a step into a user-defined type.
Syntax
Use the macro CUSTOM_PARAMETER:
CUSTOM_PARAMETER(function_name,
"{custom_type}",
R"(regex_pattern)",
"Description")
{
// access regex capture groups via CUKE_PARAM_ARG(index)
// return the desired type
}
Note
It is recommended to use raw string literals R"( ... )" for the regex pattern.
Raw strings allow you to write backslashes and quotes literally, avoiding excessive escaping.
You’ll notice it in the examples below.
A few notes on CUKE_PARAMETER:
- The callback must return the specified type.
- Capture groups in the regex start counting from 1.
- The description is used in catalogs for documentation purposes and does not affect scenario execution.
Example: Items with Weight
Suppose you want to match a step like:
When I put 30 kilograms of apples in it
You can define a custom parameter type for the quantity and item:
CUSTOM_PARAMETER(item_with_weight,
"{item with weight}",
R"((\d+) kilograms? of (\w+))",
"Item and its weight in kilograms")
{
int weight = CUKE_PARAM_ARG(1);
std::string item = CUKE_PARAM_ARG(2);
return std::make_pair(weight, item); // returns a pair<int, std::string>
}
Then, the step definition can consume the value:
WHEN(add_item_with_weight, "I put {item with weight} in it")
{
std::pair<int, std::string> item_with_weight = CUKE_ARG(1);
box& b = cuke::context<box>();
b.add_item(item_with_weight.second);
b.add_weight(item_with_weight.first);
}
Example Scenario:
Scenario: Items with more weight
Given An empty box
When I put 30 kilograms of apples in it
Then The box weights 30 kilograms
And apples are in the box
Example: Shipping Date
Suppose you want to handle a shipping date in your scenarios. First, define a structured type:
struct date
{
int year{0};
int month{0};
int day{0};
auto operator<=>(const date& other) const = default;
std::string to_string() { return std::format("{}-{}-{}", year, month, day); }
};
Define a custom parameter type that parses the date from a step:
CUSTOM_PARAMETER(custom_parameter_date,
"{date}",
R"((\d{4})-(\d{2})-(\d{2}))",
"shipping date")
{
date shipping_date;
shipping_date.year = CUKE_PARAM_ARG(1).as<int>();
shipping_date.month = CUKE_PARAM_ARG(2).as<int>();
shipping_date.day = CUKE_PARAM_ARG(3).as<int>();
return shipping_date;
}
Consume the value in a step definition:
WHEN(ship_the_box, "The box gets shipped at {date}")
{
date shipping_date = CUKE_ARG(1);
cuke::context<date>(shipping_date); // store in scenario context
}
Use it later in another step:
THEN(box_label, "The box is labeled with: {string}")
{
std::string shipping_date = cuke::context<date>().to_string();
std::string box_label = CUKE_ARG(1);
cuke::is_true(box_label.find(shipping_date) != std::string::npos);
}
Example Scenario:
Scenario: Ship the box
Given An empty box
When The box gets shipped at 2025-11-12
Then The box is labeled with: "box shipped at 2025-11-12"