How to document cucumber js step definitions with JsDoc

A photograph of the author: Brandon Whitaker

By: Brandon Whitaker

The main benefit of incorporating cucumber into your test framework is that it enables all members of a development team to bridge their understanding of the system through step definitions written in the Gherkin syntax, regardless of their technical abilities.

But how do non-technical team members such as product owners, business analysts etc know what step definitions already exist in the test project? And what do each of the step definitions do in the hooks?

Usually these people would need to get testers / developers who have written the test framework to show them hooks in code files and then dig into the codebase to explain what each of them do.

The solution is to document our implementation code using a library such as JsDoc. This will give us a website that we can host on a company platform such as Jira that business people can refer to. Doing this enables team members to be self-sufficient and build confidence in writing features and scenarios without needing to heavily rely upon technical members of the team.

The Problem

With cucumber-js, when we define hooks for our step definitions we are just invoking a method call like so;

Given(‘I go to the homepage’, (callback) => callback());

JsDoc is not built to document function calls. It is primarily built to document classes, methods and property declarations, not invocations.

The Workaround

A couple of work arounds for this exist. Firstly, you can define the function invocation as a property;

/**
 * @property Given_I_go_to_the_homepage
 * Load up the hopmepage
 */
Given('I go to the homepage', (callback) => callback());

Alternatively, you can create a custom tag such as @Given

/**
 * @Given I go to the homepage
 * Load up the home page
 */

Given(‘I go to the homepage’, (callback) => callback());

But each of these of these work arounds seem a bit of a hack and do not produce very nice documentation when JsDoc does its magic.

The Solution

A solution that creates nice documentation and gives you the ability to use JsDoc without hacking it is to declare a function for each of your hooks.

/** @module givens */
/**
 * the customer is taken to the homepage
 * @description loads the homepage in the selected browser
 * @example When I go to the homepage
 * @returns {Promise<*>} - Result
 */
const iGoToTheHomepage = async () => {
  await homepage.load();
};

Given('I go to the home page', (callback) => {
  iGoToTheHomepage().then(() => callback());
})

This will then produce the following documentation (foodoc template):

Image for post

You can now add clear documentation and provide examples of how to use each of the step definitions. Additionally, if your step definition contains parameters you can now document each of those parameters and describe what valid inputs are to the reader.

/** @module whens */

/**
 * the customer enters in their credentials in the login screen
 * @param {string} email The users email address
 * @param {string} password The users password
 * @description enters the provided credentials into the login field
 * @example When I enter my example@me.co.ck and password
 * @example When I enter my <email> and <password>
 * @returns {Promise<Boolean>} - Result
 */
const iEnterMyEmailAndPassword = async (email, password) => {
  return homepage.enterCredentials(email, password);
};

When('I enter my {string} and {string}', (email, password, callback) => {
  iEnterMyEmailAndPassword(email, password).then((result) => callback(result ? null : 'failed'));
})

Which will produce the following documentation.

Image for post

Finally, to organise all your definitions you will want to add a module definition comment at the top of each file:

/ @module givens /

This will tell JsDocs to group the function definitions together and produce a section for each module defined. You can use the same module comment on multiple files and they will be grouped together on the output website.

Personally I like to organise my modules into contextualised areas of the domain for easier use. So create a hook file for each area of the website and then add the relevant module comment at the top of the files.

/ @module website/account-management */

/ @module website/shopping-basket */

This makes it easier to find the hook that you need. Without thinking about whether the step definition is primarily a Given/When/Then.

But, are you not just creating an unnecessary abstraction with additional methods, purely to satisfy the requirement of documenting your code?

This was my initial concern when experimenting with this design pattern. However, as I built my framework to a decent size a couple of benefits with creating these additional methods emerged.

  1. Reusing core methods for similar step definitions

Often the business want to define very similar step definitions but do not want to use a parameterised hook for readability purposes. Well with the abstracted method you can have multiple step definitions utilising a single function definition.

/** @module website/authentication */

/**
 * the customer enters in their credentials in the login screen
 * @param {string} email The users email address
 * @param {string} password The users password
 * @description enters the provided credentials into the login field
 * @example When I enter my example@me.co.ck and password
 * @example When I enter my <email> and <password>
 * @example When I enter successful credentials
 * @returns {Promise<Boolean>} - Result
 */
const iEnterMyEmailAndPassword = async (email, password) => {
  return homepage.enterCredentials(email, password);
};

When('I enter my {string} and {string}', (email, password, callback) => {
  iEnterMyEmailAndPassword(email, password).then((result) => callback(result ? null : 'failed'));
});

When('I enter successful credentials, (callback) => {
  iEnterMyEmailAndPassword('me@example.com', 'password').then((result) => callback(result ? null : 'failed'));
});

2. Deprecating step definitions

This approach allows you to have a step definition that is being used by a scenario feature, but you can remove the comment from the implementation function which will stop it being ‘advertised’ in the documentation.

Alternatively, you could keep it in place and add the Deprecated (@deprecated) tag to the code comment which will show the user that it should not be used.

@AvermentDigital on TwitterFacebook and Instagram