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.
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.