Executing BDD tests with Playwright

Executing BDD tests with Playwright

For a while now, anytime that I want to be able to use .feature files to write my automated tests in a BDD (Behaviour Driven Development) like manner i've had to rely on Cucumber-js to run my tests. Cucumber-js is a test runner that will process .feature files and run each test as specified. A feature file might look like:

Scenario: I search for hashnode
Given I am on the google homepage
When I search for 'hashnode'
Then I am presented with results based upon the 'hashnode' search term

Cucumber-js is great and you can hook into the Playwright apis to be able to interact with the browser on top of it.


As the test runner is not Playwright this means that you lose out on being able to make use of all the cool Playwright features that get released. Such as:

  1. The Playwright ui for debugging tests.

  2. Sharding your test collection.

  3. A faster test runner.

  4. (Public Preview) Azure Playwright - run the tests from your machine, but let a cloud machine host the browsers and interact with them.

  5. Built-in test reporters.

And much more, but you get the point. By using Cucumber-js as our test runner, we lose out on the full blown benefits that integrating 100% with Playwright gives us. However, this is the only way forward to test projects that want to express themselves in a BDD-like manner with gherkin based feature files.

A new discovery

Recently I stumbled across a repository that really impressed me (GitHub - vitalets/playwright-bdd: BDD testing with Playwright runner).

This repo contains a package that essentially allows you to port across your existing Cucumber-js project and make it compatible with the Playwright test runner. Or use it in conjunction with a new Playwright test solution.

For existing solutions in Cucumber-js you can choose to partially migrate, whereby you keep a lot of your old cucumber code looking like this:

// test.steps.ts
import { Given, When, Then } from '@cucumber/cucumber';
import { CustomWorld } from './world';
import { expect } from '@playwright/test';

Given<CustomWorld>('I open url {string}', async function (url: string) {
  await this.page.goto(url);

When<CustomWorld>('I click link {string}', async function (name: string) {
  await this.page.getByRole('link', { name }).click();

Then<CustomWorld>('I see in title {string}', async function (keyword: string) {
  await expect(this.page).toHaveTitle(new RegExp(keyword));

Or you can choose to fully migrate, the main differences being that you make use of Playwright fixtures instead of relying on your CustomWorld and you don't use before/after hooks. Looking something like this:

// test.steps.ts
import { createBdd } from 'playwright-bdd';

const { Given, When, Then } = createBdd();

Given('I open url {string}', async ({ page }, url: string) => {
  await page.goto(url);

When('I click link {string}', async ({ page }, name: string) => {
  await page.getByRole('link', { name }).click();

The flexibility to be able to choose whether to fully migrate or partially is awesome and the package fully supports both methods when it comes to making your solution work with Playwrights test runner.

So how does it actually work then?

Playwright-bdd will scan all of your feature files and create a bunch of .spec.js files that reflect them. It will go and create Playwright tests that detail all of the test steps inside of them.

I have a feature file that looks like this:

// test.feature

Feature: Playwright site
    Scenario: Check title
        Given I open url "https://playwright.dev"
        When I click link "Get started"
        Then I see in title "Playwright"

Here's what a sample output might look like when ran through the generator using the command npx bddgen:

// test.spec.js
import { test } from 'playwright-bdd';

test.describe('Playwright site', () => {
  test('Check title', async ({ Given, When, Then }) => {
    await Given('I open url "https://playwright.dev"');
    await When('I click link "Get started"');
    await Then('I see in title "Playwright"');

Now when we run npx playwright test it will detect this new test after tweaking our config and run the tests under the Playwright test runner.

This essentially means that we can write our tests in a BDD like manner and get the full benefits of Playwright's test framework, with little major changes to our codebase and just an extra step in the development process with npx bddgen being launched after any feature file updates.

Closing thoughts

Overall I think this tool is amazing and I would highly recommend anyone to give it a go. Being able to use Playwright's full capabilities at little code change expense and saying goodbye to Cucumber-js is really worth it.

The repo is maintained mostly by one developer but there is a lot of ambition and direction for the project going forward. See their roadmap for 2024 here Project roadmap for 2024 · Issue #83 · vitalets/playwright-bdd · GitHub.

Their docs are quite comprehensive and provide all that you need to get going with a new project / existing one and relevant migration guides.

Let me know what you think!