Or press ESC to close.

Advanced Parameter Types and Smart Data Table Handling in Cucumber

Jun 22nd 2025 18 min read
medium
javascriptES6
nodejs20.15.0
cucumber11.3.0
bdd
gherkin

Behavior-Driven Development (BDD) with Cucumber makes test scenarios easy to readโ€”but maintaining them as systems grow can become tricky. In this post, we'll explore how to use custom parameter types and data table transformations in Cucumber with JavaScript to keep our step definitions clean, expressive, and close to our domain language. We'll see how turning raw strings and tables into rich objects like Email, UserRole, and Product makes our tests not just easier to write, but also easier to trust and scale.

Setting Up Our Project

Before we explore how to extend Cucumber with custom parameter types and data table transformations, let's start by setting up a clean and scalable project structure. Here's how our project is organized:

  • ๐Ÿ“ project/
    • ๐Ÿ“ features/
      • ๐Ÿ“ step_definitions/
        • ๐Ÿ“„ parameter_types.js
        • ๐Ÿ“„ user_steps.js
        • ๐Ÿ“„ product_steps.js
        • ๐Ÿ“„ table_transformations.js
      • ๐Ÿ“„ user_management.feature
      • ๐Ÿ“„ product_management.feature
      • ๐Ÿ“„ user_table_management.feature
    • ๐Ÿ“ src/
      • ๐Ÿ“ models/
        • ๐Ÿ“„ Email.js
        • ๐Ÿ“„ Product.js
        • ๐Ÿ“„ User.js
        • ๐Ÿ“„ UserRole.js
    • ๐Ÿ“„ package.json

This layout keeps our feature files close to their corresponding step logic, while isolating our core application logic in the src directory.

Required Dependencies

To get started with Cucumber.js, we run the following command:

                
npm init -y
npm install --save-dev @cucumber/cucumber
                

We can now create and run BDD-style tests using Gherkin syntax with step definitions powered by JavaScript.

Custom Parameter Types in Cucumber

In Cucumber, step definitions often extract values using simple placeholders like {string} or {int}. But what if our steps contain domain-specific values like emails, roles, or formatted prices?

That's where custom parameter types come in. They allow us to define:

Anatomy of a Parameter Type

Each custom parameter type is defined using:

Let's walk through each custom type used in our parameter_types.js.

Email Parameter Type

Why this implementation?

                
defineParameterType({
  name: "email",
  regexp: /[^\s@]+@[^\s@]+\.[^\s@]+/,
  transformer: (emailString) => {
    return new Email(emailString);
  },
});
                

This ensures invalid emails are caught early and gives us rich email handling in our steps.

User Role Parameter Type

Why this implementation?

                
defineParameterType({
  name: "user_role",
  regexp: /admin|user|guest|moderator/,
  transformer: (roleString) => {
    return new UserRole(roleString);
  },
});
                
Money Parameter Type

Why this implementation?

                
defineParameterType({
  name: "money",
  regexp: /\$?(\d+(?:\.\d{2})?)/,
  transformer: (amountString) => {
    const amount = parseFloat(amountString.replace("$", ""));
    return {
      amount: amount,
      cents: Math.round(amount * 100),
      currency: "USD",
      toString: () => `$${amount.toFixed(2)}`,
    };
  },
});
                

This lets us write money-based steps naturally, like Given I have $150.50 in my account, while working with precise cent-based values in logic.

Date Parameter Type

Why this implementation?

                
defineParameterType({
  name: "date",
  regexp:
    /(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{4}|today|tomorrow|yesterday)/,
  transformer: (dateString) => {
    let date;
    switch (dateString.toLowerCase()) {
      case "today":
        date = new Date();
        break;
      case "tomorrow":
        date = new Date();
        date.setDate(date.getDate() + 1);
        break;
      case "yesterday":
        date = new Date();
        date.setDate(date.getDate() - 1);
        break;
      default:
        date = new Date(dateString);
    }

    return {
      value: date,
      format: (formatString = "YYYY-MM-DD") => {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, "0");
        const day = String(date.getDate()).padStart(2, "0");
        return `${year}-${month}-${day}`;
      },
      isWeekend: () => {
        const dayOfWeek = date.getDay();
        return dayOfWeek === 0 || dayOfWeek === 6;
      },
    };
  },
});
                

By handling formatting and logic upfront, our step definitions stay focused on test intentโ€”not parsing.

Feature Usage Examples

Here's how these parameter types are used in .feature files and what they resolve to:

Gherkin Step Example Parameter Type Transformed Value
Given a user with email john@example.com and role admin {email} {user_role} Email and UserRole instances
Given I have $150.50 in my account {money} { amount: 150.5, cents: 15050, ... }
Given today is tomorrow {date} { value: Date, isWeekend(): boolean, ... }
Then the user should have admin privileges {user_role} UserRole instance with permission logic

These transformations allow us to write natural language steps while working with structured, validated, and behavior-rich data in our test code.

Writing Gherkin Scenarios with Custom Types

Once we've defined our custom parameter types in parameter_types.js, we can use them directly in our Gherkin step definitions using curly brace syntaxโ€”just like built-in types ({int}, {string}, etc.).

This section shows how our scenarios in user_management.feature leverage {email}, {user_role}, {money}, and {date} to make steps clean, expressive, and strongly typed.

Scenario 1: Creating Users with Email and Role
                
Scenario: Creating users with different roles
  Given a user with email john@example.com and role admin
  And a user with email jane@example.com and role user
  When I login as john@example.com
  Then the user should have admin privileges
  And the user should be able to "manage_users"
                

How It Works:

In our step definition:

                        
Given("a user with email {email} and role {user_role}", function (email, role) {
  const user = new User({ email, role });
  users.push(user);
  this.lastCreatedUser = user;
});
                        

The test receives email and role as fully-formed objects, not raw strings. This allows our User model to validate and behave exactly like it would in production code.

Scenario 2: Money Transformation
                
Scenario: Money handling
  Given I have $150.50 in my account
  Then my balance should be $150.50
                

How It Works:

{money}: Matches strings like $150.50 or 150.50 and transforms them into a structured object:

                
{
  amount: 150.5,
  cents: 15050,
  currency: 'USD',
  toString: () => "$150.50"
}
                

Step definition:

                
Given("I have {money} in my account", function (amount) {
  this.accountBalance = amount;
});

Then("my balance should be {money}", function (expectedAmount) {
  assert.strictEqual(this.accountBalance.cents, expectedAmount.cents);
});
                

This keeps our balance logic consistent, avoids string comparison issues, and supports currency-aware logic.

Scenario 3: Flexible Date Handling
                
Scenario: Date handling
  Given today is tomorrow
  Then the date should be a weekend
                

How It Works:

{date}: Accepts keywords (today, tomorrow, yesterday) or formatted dates (2025-06-01, 06/01/2025) and transforms them into an object with a Date value and utilities like isWeekend().

                
Given("today is {date}", function (date) {
  this.currentDate = date;
});
Then("the date should be a weekend", function () {
  assert(this.currentDate.isWeekend(), "Expected date to be a weekend");
});
                

This allows natural language like tomorrow while still testing real logic based on calendar dates.

Cucumber automatically links {email}, {user_role}, {money}, and {date} in our step definitions to the corresponding custom parameter types defined earlier. This is based on the name field used in defineParameterType.



We don't need to manually parse or convert valuesโ€”they arrive in our step functions already validated and modeled.

Transforming Data Tables into Domain Models

Cucumber supports data tables in Gherkin steps, which are great for passing structured input into our tests. But instead of manually parsing raw strings every time, we can build reusable transformation helpers that turn data tables into real domain objects like Product, User, or even plain key-value objects.

Understanding dataTable.hashes() vs. dataTable.rowsHash()

dataTable.hashes() converts a horizontal table into an array of objects. We use this when each row represents a new object (e.g., a list of users or products).

Example Input:

                
| name  | price | category |
| Phone | $999  | gadgets  |
| Mouse | $49   | gadgets  |
                

Output:

                
[
  { name: 'Phone', price: '$999', category: 'gadgets' },
  { name: 'Mouse', price: '$49', category: 'gadgets' }
]
                

dataTable.rowsHash() converts a vertical table into a single object, where the first column becomes keys and the second column becomes values.

Example Input:

                
| name     | Magic Mouse   |
| price    | $79.99        |
| category | accessories   |
                

Output:

                
{
  name: 'Magic Mouse',
  price: '$79.99',
  category: 'accessories'
}
                
Transforming Product Tables

The transformProductTable helper converts horizontal tables of multiple products into an array of Product instances. It extracts fields like price, tags, and inStock, and wraps them with the appropriate logic.

Use case: Feature scenarios that define a product catalog with multiple entries.

                
function transformProductTable(table) {
  return table.hashes().map((row) => {
    return new Product({
      name: row.name,
      price: {
        amount: parseFloat(row.price.replace("$", "")),
        cents: Math.round(parseFloat(row.price.replace("$", "")) * 100),
        currency: "USD",
        toString: () => row.price,
      },
      category: row.category,
      inStock: row.in_stock === "true",
      description: row.description || "",
      tags: row.tags ? row.tags.split(",").map((tag) => tag.trim()) : [],
    });
  });
}
                

This makes it easy to populate a product list in one line inside a step definition:

                
Given("the following products exist:", function (dataTable) {
  const productList = transformProductTable(dataTable);
  products.push(...productList);
});
                
Transforming User Tables

The transformUserTable function converts horizontal tables into an array of User instances, internally utilizing Email and UserRole parameter types for consistency.

Use case: Creating multiple users with emails, names, and roles.

                
function transformUserTable(table) {
  return table.hashes().map((row) => {
    return new User({
      email: new Email(row.email),
      role: new UserRole(row.role),
      name: row.name,
    });
  });
}
                

Feature example:

                
Given the following users exist:
  | name       | email            | role      |
  | John Smith | john@example.com | admin     |
  | Jane Doe   | jane@example.com | user      |
                

Step definition:

                
Given("the following users exist:", function (dataTable) {
  const userList = transformUserTable(dataTable);
  users.push(...userList);
});
                
Transforming a Single Product with a Vertical Table

When a feature step describes a single entity in a vertical format, the transformVerticalTable utility turns it into a plain object, which can be passed to a class constructor or used directly.

                
function transformVerticalTable(table) {
  const obj = {};
  for (const [key, value] of Object.entries(table.rowsHash())) {
    obj[key] = value;
  }
  return obj;
}
                

Feature example:

                
Given a product with these details:
  | name        | Premium Widget     |
  | price       | $29.99             |
  | category    | gadgets            |
  | description | A fantastic gadget |
  | tags        | premium,new,useful |
                

Step definition:

                
Given("a product with these details:", function (dataTable) {
  const productData = transformVerticalTable(dataTable);
  const product = new Product({
    name: productData.name,
    price: {
      amount: parseFloat(productData.price.replace("$", "")),
      cents: Math.round(parseFloat(productData.price.replace("$", "")) * 100),
      currency: "USD",
      toString: () => productData.price,
    },
    category: productData.category,
    description: productData.description,
    tags: productData.tags
      ? productData.tags.split(",").map((tag) => tag.trim())
      : [],
  });

  products.push(product);
  this.lastCreatedProduct = product;
});
                

Step Definitions Using Table Transformations

After defining transformation helpers for data tables, we can plug them into our step definitions to work with domain objects instead of raw strings. This improves readability, reduces duplication, and keeps test logic closely aligned with business logic.

Product Step Definitions

Given the following products exist: This step uses a horizontal table with multiple products. Instead of parsing the table manually, it uses transformProductTable() to create fully-formed Product instances.

                
Given("the following products exist:", function (dataTable) {
  const productList = transformProductTable(dataTable);
  products.push(...productList);
  this.products = products;
});
                

Feature Example:

                
Given the following products exist:
  | name        | price   | category    | in_stock | tags           |
  | iPhone 15   | $999.99 | electronics | true     | phone,premium  |
  | MacBook Pro | $2499   | computers   | true     | laptop,premium |
                

This lets us simulate a product catalog without needing to duplicate object creation logic across tests.

Given a product with these details: This step creates a single product using a vertical key-value table. The transformVerticalTable() helper converts the input into a flat object, and the step builds a Product from it.

                
Given("a product with these details:", function (dataTable) {
  const productData = transformVerticalTable(dataTable);

  const product = new Product({
    name: productData.name,
    price: {
      amount: parseFloat(productData.price.replace("$", "")),
      cents: Math.round(parseFloat(productData.price.replace("$", "")) * 100),
      currency: "USD",
      toString: () => productData.price,
    },
    category: productData.category,
    description: productData.description,
    tags: productData.tags
      ? productData.tags.split(",").map((tag) => tag.trim())
      : [],
  });

  products.push(product);
  this.lastCreatedProduct = product;
});
                
Searching and Assertions

The product steps also include logic for filtering by category or tag, and for validating product details.

                
When("I search for products in category {string}", function (category) {
  searchResults = products.filter((product) => product.category === category);
});

Then("I should find {int} product(s)", function (expectedCount) {
  assert.strictEqual(searchResults.length, expectedCount);
});
                

And more detailed validation:

                
Then("the products should have the following details:", function (dataTable) {
  const expectedProducts = transformProductTable(dataTable);

  expectedProducts.forEach((expectedProduct) => {
    const actualProduct = products.find((p) => p.name === expectedProduct.name);
    assert(actualProduct, `Product ${expectedProduct.name} not found`);
    assert.strictEqual(actualProduct.category, expectedProduct.category);
    assert.strictEqual(actualProduct.price.cents, expectedProduct.price.cents);
  });
});
                

This makes end-to-end product tests robust while keeping the syntax concise.

User Step Definitions

Given the following users exist: Just like products, users can be created using horizontal tables and the transformUserTable() helper:

                
Given("the following users exist:", function (dataTable) {
  const userList = transformUserTable(dataTable);
  users.push(...userList);
  this.users = users;
});
                

Feature Example:

                
Given the following users exist:
  | name       | email            | role      |
  | John Smith | john@example.com | admin     |
  | Jane Doe   | jane@example.com | user      |
                

Each user is initialized with an Email and UserRole object, thanks to the transformation logic.

Login Simulation and Permission Checks

The user steps go beyond just creationโ€”they simulate login and assert behavior based on role permissions:

                
When("I login as {email}", function (email) {
  currentUser = users.find((user) => user.email.value === email.value);
  if (!currentUser) {
    throw new Error(`No user found with email: ${email.value}`);
  }
});

Then("the user should have {user_role} privileges", function (expectedRole) {
  assert.strictEqual(currentUser.role.value, expectedRole.value);
});

Then("the user should be able to {string}", function (permission) {
  assert(
    currentUser.hasPermission(permission),
    `User with role ${currentUser.role.value} should have ${permission} permission`
  );
});
                

This tight integration between transformed input and test logic makes it easy to verify complex role-based behavior with just a few lines of Gherkin.

By transforming tables into rich objects early, our step definitions stay focused on behavior, not parsing.

This leads to cleaner, more expressive, and easier-to-maintain tests.

Writing Feature Files with Meaningful Tables

Well-structured .feature files serve as living documentation and help stakeholders understand system behavior. By combining custom parameter types with transformed data tables, our Gherkin scenarios become both readable and executable.

product_management.feature: Advanced Product Scenarios

This file demonstrates multiple ways to define product data:

Horizontal tables for bulk creation:

                
Given the following products exist:
  | name        | price   | category    | in_stock | tags           |
  | iPhone 15   | $999.99 | electronics | true     | phone,premium  |
                

Vertical tables for individual product setup:

                
Given a product with these details:
  | name        | Premium Widget     |
  | price       | $29.99             |
  | category    | gadgets            |
                

This approach makes it easy to test different use cases like search, filtering, and data validation with minimal duplication.

user_management.feature & user_table_management.feature

These files showcase:

✷ Using custom types ({email}, {user_role}) directly in steps:

                
Given a user with email john@example.com and role admin
                

✷ Creating multiple users using a data table:

                
Given the following users exist:
  | name       | email            | role  |
  | Jane Doe   | jane@example.com | user  |
                

This keeps role-based testing scenarios clear, even for non-technical stakeholders.

Benefits of Using Custom Types and Table Transformers

Using custom parameter types and table transformation helpers offers several key advantages that scale well with growing projects:

Best Practices

To get the most out of custom parameter types and data table transformations, follow these practical guidelines:

Conclusion

Custom parameter types and table transformations in Cucumber bring structure, clarity, and power to our test automation. By converting raw strings and tables into rich domain objects, we write tests that are easier to read, more reusable, and closely aligned with real application behavior.

This approach reduces duplication, improves maintainability, and keeps our .feature files readable for both technical and non-technical team members.

The complete code examples from this blog are available on our GitHub page โ€” feel free to explore, clone, and adapt them to your own projects.