Would you like to TALK about it?

I'm in a Pickle - Configuring Gherkin with NX Workspaces

April 20, 2020

Why NX?

I believe, that every project should start with a proper development environment setup. NX does that or at least gives us a head-start. When you generate a project in a monorepo structure, it provides you with very good tools already set up. I encourage you to try it out, it has helped me deliver well tested and stable code for short deadlines. By default, it sets up Jest as the unit test framework, and it also generates a well set up cypress test environment for every front-end project.

However, there comes a time, when you encounter a project, where the client has been using Selenium driven e2e tests with Gherkin as their test language. There is a good reason for that because Gherkin is the format for cucumber specifications. It is a language abstraction which helps you to describe business behaviour without the need to go into details of the implementation. This text acts as documentation, and a skeleton for your automated tests. Although, setting it up inside an NX workspace might require some effort.

I turned this blog post into a pickle

Set up Gherkin

Cypress uses JavaScript for writing the test cases, but NX ships it with nice TypeScript support. We would like to support Gherkin, therefore, we need to install cypress-cucumber-preprocessor as a dev dependency.

npm install cypress-cucumber-preprocessor cucumber-html-reporter --save-dev

With the packages installed, let’s set up a configuration file in our root folder, so let’s add the cypress-cucumber-preprocessor.config.js file to our root folder. We set up our universe config, so each of our cypress tests will look for their step definitions in their respective folders.

const path = require("path")

const stepDefinitionsPath = path.resolve(process.cwd(), "./src/integration")
const outputFolder = path.resolve(process.cwd(), "../../cyreport/cucumber-json")

module.exports = {
  nonGlobalStepDefinitions: true,
  stepDefinitions: stepDefinitionsPath,
  cucumberJson: {
    generate: true,
    outputFolder: outputFolder,
    filePrefix: "",
    fileSuffix: ".cucumber",
  },
}

It is important that this file had the .config.js ending, otherwise, your step definitions will not be found. Notice, that we have configured the stepDefinitionPath variable, so it looks for the feature files from the current working directory. This is important because that is going to be the folder from where cypress is going to run. We created an outputFolder variable as well because we are going to need it to generate our cucumber-json file for our reports, but more on that later. Finally, we export our universe config, with setting the nonGlobalStepDefinitions property to true. We do this because we want our step definitions to be under the {your_e2e_folder}/integration/common folder.

Migrate our tests

A freshly generated NX e2e project uses the following folder structure:

project-e2e
|_src
| |_fixtures
| | |_example.json
| |_integration
| | |_app.spec.ts
| |_plugins
| | |_index.js
| |_support
| | |_app.po.ts
| | |_commands.ts
| | |_index.ts
|_cypress.json
|_tsconfig.e2e.json
|_tsconfig.json
|_tslint.json

The default test looks like the following:

// app.po.ts
export const getGreeting = () => cy.get("h1")

// app.spec.ts
import { getGreeting } from "../support/app.po"

describe("sandbox", () => {
  beforeEach(() => cy.visit("/"))

  it("should display welcome message", () => {
    // Custom command example, see `../support/commands.ts` file
    cy.login("my-email@something.com", "myPassword")

    // Function helper example, see `../support/app.po.ts` file
    getGreeting().contains("Welcome to app!")
  })
})

What does this test do? Before every test, the application is opened at the / url. The cy.login() method is a custom command added in the commands.ts file. It simulates a login step. Then finally, checks if the page displays Welcome to app! inside an h1 HTML tag. Let’s implement the same in Gherkin. We can delete the app.spec.ts and the app.po.ts files and create a Landing.feature file with the following content:

Feature: Landing page

Scenario: The application displays the welcome message.
  Given the user is logged in
  Then the "Welcome to app!" message is displayed

We create two folders near our feature file, the Landing and the common. The common folder contains our shared steps, the Landing folder deals with the feature-specific steps. Make sure that you use the same name for these steps as you use for your .feature file. In the common/common.steps.ts, let’s add the following:

import { Before } from "cypress-cucumber-preprocessor/steps"

Before(() => {
  cy.server()
})

This will make sure, that we can stub requests in steps. Be careful though, the order of the steps matters. In our Landing/landing.steps.ts file, we add our Given and Then steps.

import { Before, Given, Then } from "cypress-cucumber-preprocessor/steps"

Before(() => {
  cy.visit("/")
})

Given("the user is logged in", () => {
  cy.login("my-email@something.com", "myPassword")
})

Then("the {string} message is displayed", (message: string) => {
  cy.get("h1").contains(message)
})

Preprocessed Pickles

Our roughly-the-same test case is ready, however, we still cannot run our tests. Our cypress runner is not-yet set up for processing .feature files, and it still expects only typescript files as test sources. We need to update our cypress configuration. Let’s update our cypress.json first:

{
  "fileServerFolder": ".",
  "fixturesFolder": "./src/fixtures",
  "integrationFolder": "./src/integration",
  "modifyObstructiveCode": false,
  "pluginsFile": "./src/plugins/index",
  "supportFile": "./src/support/index.ts",
  "video": true,
  "videosFolder": "../../dist/cypress/apps/sandbox-e2e/videos",
  "screenshotsFolder": "../../dist/cypress/apps/sandbox-e2e/screenshots",
  "chromeWebSecurity": false,
  "testFiles": "**/*.{feature,features}"
}

We also need to update our cypress plugins file. Let’s open the plugins/index.js file and replace its contents with the following:

Update (2021.02.02.) As of Cypress v4.4.0, TypeScript is supported out-of-the-box.

const browserify = require('@cypress/browserify-preprocessor');
const cucumber = require('cypress-cucumber-preprocessor').default;
const resolve = require('resolve');

module.exports = (on, config) => {
  const options = {
    ...browserify.defaultOptions,
    typescript: resolve.sync('typescript', { baseDir: config.projectRoot }),
  };

  on('file:preprocessor', cucumber(options));
};

We don’t want to override the existing typescript compilation solution provided by our NX workspace, we just want to extend it. We add two loaders, one for .feature files and one for .features files. In the cypress plugin setup method that we export we need to make some changes. Our webpackConfig needs to set the node options to 'empty', so compilation does not throw node-specific errors. We push our feature configurations into the rules, and we set the preprocessor to use our new config.

Now if you run your cypress tests with npm run e2e it uses Gherkin. One note though, if you are used to tag-based test running with CucumberJS that does not work well in an NX workspace. At least I have not found a proper way to pass the TAGS environment variables in the command line. That said, all the debugging capabilities are present if you open cypress in their application, you can select your tests, and you can debug them easily.

However, If anybody can provide a good solution for tags, I’m open to suggestions.

Lt. Pickle reports for duty

One of the reasons to use cucumber is human-readable test cases. When your Product Owner wants to validate properly how a feature set works you can present them with cool Cucumber test run reports. In our initial config setup, we have already added configuration to generate a cypress report with a cucumber.json file. We just need to add a tool to generate a nice report. Let’s create a generate-cucumber-report.js file in our tools folder:

const path = require("path")
const reporter = require("cucumber-html-reporter")
const chalk = require("chalk")

const options = {
  theme: "bootstrap",
  jsonDir: path.join(process.cwd(), "cyreport/cucumber-json"),
  output: path.join(process.cwd(), "cyreport/cucumber_report.html"),
  reportSuiteAsScenarios: true,
  scenarioTimestamp: true,
  launchReport: false,
  storeScreenshots: true,
  noInlineScreenshots: false,
}

try {
  reporter.generate(options)
} catch (e) {
  console.log(chalk.red(`Could not generate cypress reports`))
  console.log(chalk.red(`${e}`))
}

We put the whole report generator method into a try-catch, because if we run our e2e tests in the affected mode, sometimes they won’t run, and report generation would break our build pipeline. Let’s update our package.json to run this generator script.

{
  "scripts": {
    "devtools:cucumber:report": "node ./tools/generate-cucumber-report.js"
  }
}

After running your e2e tests, running npm run devtools:cucumber:report in the terminal would generate a report. We can find it in the cyreport folder.


This guide could not have been written without several hours of reading GitHub issues. I’d like to thank the open-source community for creating these awesome tools, and for documenting them properly. Huge thanks for the people who found solutions and workarounds and comment them on GitHub issues. I wanted to document my final solution, so it might help others in the future.


Balázs Tápai

Written by Balázs Tápai.
I will make you believe that I'm secretly three senior engineers in a trench-coat. I overcome complex issues with ease and complete tasks at an impressive speed. I consistently guide teams to achieve their milestones and goals. I have a comprehensive skill set spanning from requirements gathering to front-end, back-end, pipelines, and deployments. I do my best to continuously grow and increase my capabilities.

You can follow me on Twitter or Github.