Master BDD in Node.js using Jest and Cucumber

BDD Fundamentals with Jest and Cucumber

Learn how to implement BDD (Behavior-Driven Development) in your Node.js projects using Jest and Cucumber

Introduction

Behavior-Driven Development (BDD) bridges the communication gap between technical and non-technical team members by using a common language to describe software behavior.

This article explores the fundamentals of how you can leverage Jest and Cucumber to help you implement BDD in Node.js projects, combining Jest’s powerful testing features with Cucumber’s human-readable test syntax.

This article is the first part in our series on BDD with Jest and Cucumber. To explore more, check out our follow-up article: Advanced BDD Techniques with Jest and Cucumber where we discuss more advanced features and best practices that will take your tests to the next level.

You can find the complete code example here.

Understanding BDD, Jest, and Jest-Cucumber

So, you want to get into Behavior-Driven Development (BDD) with Node.js? And most likely you already have Jest as a test runner, and you’ve heard about a thing called Cucumber?

Good news, you are in the right place.

What is Behavior-Driven Development (BDD)?

Ever been in a meeting where the business folks, the testers, and the coders are all talking different languages? Yeah, me too. BDD tries to fix that mess.

The big idea is using a common language (they call it Ubiquitous Language) that everyone understands to describe how software should behave from a user’s point of view. It’s kinda like building with LEGOs based on instructions everyone agrees on.

Why bother with BDD for your project?

Actually, a reasonable question; specifically, it needs some extra work from the team. But let me try to convince you:

  • Better Communication: When your tests read like plain English, anyone – product owners, testers, new devs – can understand what the system is supposed to do without needing a technical deep dive. This significantly reduces misunderstandings.
  • Better Documentation: Also, it naturally leads to better documentation; your feature files become living documentation that stays up-to-date because they’re tied to your tests.
  • Business Fullfillment: Plus, it encourages writing code that’s focused on user needs from the start.

What is Jest-Cucumber?

Most likely, you know Jest, but what is Cucumber?

Cucumber is a BDD tool that allows teams to write human-readable tests that describe application behavior in plain language using Gherkin syntax.

Right, so what’s Jest-Cucumber then? Think of it as the translator. It takes those plain-English BDD descriptions written in Gherkin syntax (Given, When, Then) and lets you run them using Jest’s engine.

Jest Integration with Cucumber

Guess what, you can use the Cucumber.js library to run Cucumber directly via its built-in command-line runner, which skips Jest entirely and gets the job done. However, doing so means sacrificing Jest’s powerful features. By integrating Cucumber with Jest, you can enjoy the best of both worlds.

  • Watch Mode: Cucumber lacks a built-in watch mode, requiring external tools like nodemon or chokidar to achieve similar functionality.
  • Parallel Execution: Cucumber runs scenarios sequentially by default, and while parallelization is possible, it’s more complex to configure compared to Jest’s automatic parallel test execution.
  • Built-in Mocking and Assertion: Cucumber does not provide built-in mocking or powerful assertions, requiring additional libraries like Sinon or Chai, whereas Jest has these features natively.
  • Configuring Jest Reporters for Cucumber: Cucumber.js has its own reports, however, you can leverage Jest’s built-in awesome reports.
  • Using Jest Matchers in Step Definitions: A key advantage of using Jest is the ability to leverage its powerful expect matchers directly in Cucumber step definitions. This improves readability and provides clearer error messages compared to Node’s built-in assert.
  • Custom Jest Extensions for BDD: You can extend Jest with domain-specific matchers (expect.extend({...})) for clearer assertions (e.g., expect(user).toBeLoggedIn()).
  • Jest Hooks: You can use Jest’s beforeAll, afterAll, beforeEach, afterEach in your *.steps.js files (or dedicated setup files specified in jest.config.js) to handle common setup/teardown logic needed for your BDD scenarios. This keeps your step definitions focused on the specific step’s logic.

And finally, for many Node.js projects already using Jest for unit/integration tests, integrating Cucumber via jest-cucumber feels natural. It keeps your tooling consistent.

Setting Up Your Node.js Project with Jest-Cucumber

Alright, convinced enough to give it a shot? Let’s get our hands dirty. Setting up isn’t too bad, really. Follow along.

Installation Steps

Open your terminal in your project folder. If you don’t have a project yet, make one:

# Make a new folder and go into it
mkdir my-bdd-project
cd my-bdd-project

# Initialize it
npm init -y

Now, let’s add the necessary packages:

# Install Jest and Jest-Cucumber
npm install --save-dev jest jest-cucumber

Basic Configuration

Now we need to tell Jest how to find and run our BDD tests. Jest-Cucumber needs a little heads-up. You can configure Jest in a jest.config.js file or directly in your package.json. I kinda prefer jest.config.js, keeps things tidy.

Create a jest.config.js file in your project root:

// jest.config.js
module.exports = {
  // Tell Jest to look for tests in files ending with .steps
  testMatch: ["**/specifications/step-definitions/**/*.steps.js"],
...

Important Bit: Jest doesn’t naturally know how to run .steps.js files. The testMatch above tells Jest which files contain the test definitions.

Recommended Project Structure:

Organizing is good. Helps later. A common way:

my-bdd-project/
├── specifications/          # Parent folder for features and step definitions
│   ├── features/            # Your Gherkin .feature files live here
│   │   └── register.feature
│   └── step-definitions/    # Your JavaScript step files
│       └── register.steps.js
├── src/                     # Your actual application code
│   └── index.js
├── jest.config.js           # Jest configuration
└── package.json             # Project manifest

This separation makes it clear where to find things. Features describe behavior, steps connect it to code.

Writing Your First BDD Test: Features and Scenarios

Okay, setup’s done (mostly). Now for the fun part: describing what your software should do in plain English! This happens in .feature files using Gherkin syntax.

Understanding Gherkin Syntax

Gherkin is a simple, structured language. It uses a few keywords to make things readable. You’ll see these a lot:

  • Feature: Name of the feature you’re describing. Usually followed by a short description (the “narrative”).
  • Scenario: A specific example or rule within that feature.
  • Given Sets up the initial state. “Given I am logged out…”
  • When Describes an action the user takes. “When I enter my username and password…”
  • Then States the expected outcome or result. “Then I should be logged in…”
  • And, But Used to chain multiple Given, When, or Then steps together logically. They behave just like the keyword they follow.

The goal is clarity. Write it so that someone who didn’t write the code can understand the flow. Think simple sentences.

💌 Enjoying this post? Subscribe to get more practical dev tips right in your inbox.

Creating Your First .feature File

Let’s make a simple example. Imagine we’re testing a basic register endpoint.

Create a file named specifications/features/register.feature:

# specifications/features/register.feature

Feature: User Registration
  As a new user
  I want to register an account
  So that I can access the system

  Scenario: Successful user registration
    Given the registration system is available
    When I submit registration with username "newuser" and password "password123"
    Then I should receive a successful registration message

See? Pretty readable, right? We have a Feature with a little story, and two Scenario examples, each with Given, When, And, Then steps. Each scenario describes one specific path or behavior. Next, we need to teach our code how to understand these steps.

Implementing Step Definitions

So you got these nice .feature files. But right now, they’re just text. They don’t do anything. We need to connect those Gherkin steps (Given, When, Then) to actual JavaScript (or TypeScript) code that Jest can run. That’s where step definition files come in.

Linking Features to Code: The Role of Step Definitions

Think of step definitions as the glue. Jest-Cucumber reads your .feature file, finds a Scenario, and then looks for matching functions in your step definition files for each Given, When, Then.

How does it do this magic? Primarily through two functions provided by jest-cucumber:

  1. loadFeature(featureFilePath): You tell it where your .feature file is.
  2. defineFeature(feature, testFn): You take a loaded feature object and wrap your test implementation inside testFn. This function provides helpers like given, when, then to define the steps.

Jest-Cucumber matches the text in your Gherkin step (like "I enter the number 5") to the pattern you provide in your step definition function.

Writing Basic Step Definition Files

Let’s write the steps for our specifications/features/register.feature. Create a file named specifications/step-definitions/register.steps.js:

// specifications/step-definitions/register.steps.js

const { defineFeature, loadFeature } = require("jest-cucumber");
const { registerUser } = require("../../test-helpers");

const feature = loadFeature("./specifications/features/register.feature");

defineFeature(feature, (test) => {
  let customWorld = {
    response: null
  };

  test("Successful user registration", ({ given, when, then }) => {
    given("the registration system is available", () => {
      // System is available by default
    });

    when(
      /^I submit registration with username "(.*)" and password "(.*)"$/,
      async (username, password) => {
        customWorld.response = await registerUser(username, password);
      }
    );

    then("I should receive a successful registration message", () => {
      expect(customWorld.response.status).toBe(201);
      expect(customWorld.response.body.message).toBe("User registered successfully");
    });
  });
});

Key things happening here:

  1. Load: We load our specific .feature file using loadFeature.
  2. Define: We use defineFeature to group the tests for this feature.
  3. Steps: Inside each test block, we use given, when, then.
    • The first argument is a string or a Regular Expression (RegExp) that must match the text in the .feature file exactly (or match the pattern).
    • Notice (.*) in the RegExp? That’s a capture group. It grabs the variable from the step text.
    • The second argument is a function that gets executed when the step runs. Any captured values (like the variables) are passed as arguments to this function.
  4. Assertions: Look inside the then step functions, you can use the powerful assertions set from Jest.

Executing Tests

Okay, features written, steps defined. Time to see if it all works!

Running tests is usually done via npm scripts in your package.json. Add one if you haven’t already:

// package.json (partial)
{
  "scripts": {
    "test": "jest"
  }
}

Now, just run it from your terminal:

npm test

Jest will pick up your jest.config.js, find the .feature files via testMatch, use Jest-Cucumber’s magic to find the corresponding .steps.js files (based on the loadFeature path and defineFeature calls), execute the steps, and report the results.

Conclusion

Implementing BDD with Jest-Cucumber in your Node.js projects offers significant benefits for team communication and software quality.

By creating human-readable tests with Gherkin and connecting them to your code through step definitions, you establish a clear understanding of how your software should behave from a user’s perspective. This approach not only improves collaboration between team members but also serves as living documentation that evolves with your codebase.

As you begin your BDD journey, remember that the goal is to focus on behavior rather than implementation details, keeping your tests aligned with real user needs.

Think about it

If you enjoyed this article, I’d truly appreciate it if you could share it—it really motivates me to keep creating more helpful content!

If you’re interested in exploring more, check out these articles.

Thanks for sticking with me until the end—I hope you found this article valuable and enjoyable!

Want more dev insights like this? Subscribe to get practical tips, tutorials, and tech deep dives delivered to your inbox. No spam, unsubscribe anytime.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top