gherkin-genie
v1.3.3
Published
A library to magically create tests from Gherkins.
Downloads
18
Maintainers
Readme
Gherkin Genie
The most simple Gherkin possible. Use your favorite test runner. Zero regular expressions. Code ready to copy and paste.
It is time to stop using complex regular expressions and wondering which is the best name for our step functions. With Gherkin Genie you will be able to quickly use gherkins with minimal effort while avoiding gerkins.
See examples for more details.
Background: this engine provides from the needs for my teachings at the University. Students were struggling with the regular expressions and the step functions, so I decided to create a new approach to the problem. This is the 7th iteration over the problem, and the improvements are huge, specially when comparing how students have improved their skills thanks to this tool. See https://github.com/drpicox/classroom--cards-game--2022.
Setup
- You already have a test runner like Jest.
Your test runner does not work by default? Leave an issue and see Custom test runners.
- Install the package:
npm install --save-dev gherkin-genie
or with yarn:
yarn add --dev gherkin-genie
- Run your tests:
npm test
Or
yarn test
Quick Start
- Create a file with your feature file.
# HelloWorld.feature
Feature: Hello World
Scenario: Running a Gherkin test
Given I am running a Gherkin test
When I run the test
Then I should see the Hello World
- Create the test that wishes to run these feature file:
// HelloWorld.spec.js
import { wish } from "gherkin-genie";
wish("./HelloWorld.feature");
- Run the tests and look at the output:
There are missing steps. Please implement them:
class WishedSteps {
givenIAmRunningAGherkinTest() {
throw new Error("Unimplemented");
}
whenIRunTheTest() {
throw new Error("Unimplemented");
}
thenIShouldSeeTheHelloWorld() {
throw new Error("Unimplemented");
}
}
- Copy and paste them to your file, and implement:
// HelloWorldSteps.js
class HelloWorldSteps {
givenIAmRunningAGherkinTest() {
// ...
}
whenIRunTheTest() {
// ...
}
thenIShouldSeeTheHelloWorld() {
// ...
}
}
- And pass to your wish:
// HelloWorld.spec.js
import { wish } from "gherkin-genie";
import { HelloWorldSteps } from "./HelloWorldSteps";
wish("./HelloWorld.feature", [HelloWorldSteps]);
Three wishes 🧞♂️
The API is ready to grant three wishes:
- Create the tests for a feature file:
wish(featureFilePath, [stepsDefinitions]);
- Get instances of other stepDefinitions (aka injection):
const instance = wish(StepDefinitionsClass);
wish({ testFn: runner.test });
There is the classic api with a different method more specific for each functionality:
- createFeatureFileTests
- get
- configuration.setTestFn
Usage
Step methods
It automatically creates the tests and the gherkin matchers for you.
It creates a test named Running a Gherkin test
and maps the steps to the functions in the HelloWorldSteps
class
as follows:
Given I am running a Gherkin test
->givenIAmRunningAGherkinTest
When I run the test
->whenIRunTheTest
Then I should see the Hello World
->thenIShouldSeeTheHelloWorld
The rules are simple:
- The step name is converted to camel case.
- Spaces are removed.
- Given / When / Then / And / But are removed.
- Numbers and non alphanumeric characters are removed.
- It allows prepend given / when / then / and / but to the method name.
Following Gherkin conventions
keywords are not taken into account when looking for a step definition.
So, any of the following step names will match the givenIAmRunningAGherkinTest
function:
Given I am running a Gherkin test
->givenIAmRunningAGherkinTest
When I am running a Gherkin test
->givenIAmRunningAGherkinTest
Then I am running a Gherkin test
->givenIAmRunningAGherkinTest
And I am running a Gherkin test
->givenIAmRunningAGherkinTest
But I am running a Gherkin test
->givenIAmRunningAGherkinTest
Numbers
Numbers are automatically converted to numbers and passed as a parameter:
Feature: Magic of Disappearing Cucumbers
Scenario: Eating 5 out of 12 cucumbers
Given I have 12 cucumbers
When I eat 5 cucumbers
Then I should have 7 cucumbers remaining
class CucumberSteps {
#count = 0;
givenIHaveNCucumbers(count: number) {
this.#count = count;
}
whenIEatNCucumbers(eaten: number) {
this.#count -= eaten;
}
thenIShouldHaveNCucumbersRemaining(left: number) {
expect(this.#count).toBe(left);
}
}
You can see that numbers are replaced by N
in the method name
and the numeric value is passed as a parameter.
Strings
Strings are automatically converted to strings and passed as a parameter:
Feature: Colors and purchases
Scenario: John buys a red car
Given "John" likes color "Red"
When "John" buys a "car"
Then a "Red" "car" should be sold
class ColorSteps {
#colors: Record<string, string> = {};
#items: Record<string, string> = {};
givenSLikesColorS(name: string, color: string) {
this.#colors[name] = color;
}
whenSBuysAS(name: string, item: string) {
this.#items[item] = this.#colors[name];
}
thenASSShouldBeSold(item: string, color: string) {
expect(this.#items[item]).toBe(color);
}
}
You can see that strings are replaced by name
and item
in the method name
and the string value is passed as a parameter.
Strings are expected to be always between double quotes, and
are replaced by S
in the method name.
Doc Strings
Doc strings are automatically converted to strings and passed as the parameter:
Feature: Blog posts
Scenario: John creates a post
Given a blog post named "Random" with Markdown body
"""
Some Title, Eh?
===============
Here is the first paragraph of my blog post. Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
"""
Then the blog post should be titled "Random"
And the blog post body should contain "Here is the first paragraph"
class PostSteps {
#title: string;
#body: string;
givenABlogPostNamedSWithMarkdownBody(title: string, docString: string) {
this.#title = title;
this.#body = docString;
}
thenTheBlogPostShouldBeTitledS(title: string) {
expect(this.#title).toBe(title);
}
andTheBlogPostBodyShouldContainS(body: string) {
expect(this.#body).toContain(body);
}
}
Tables
Tables are automatically converted to arrays of objects and passed as the last parameter:
Feature: User handles
Scenario: Users have twitter handles
Given the following users exist:
| name | email | twitter |
| Aslak | [email protected] | @aslak_hellesoy |
| Julien | [email protected] | @jbpros |
| Matt | [email protected] | @mattwynne |
Then the user "Aslak" should have the twitter handle "@aslak_hellesoy"
And the user "Julien" should have the twitter handle "@jbpros"
And the user "Matt" should have the twitter handle "@mattwynne"
type TableEntry = { name: string; email: string; twitter: string };
class ExampleSteps {
#table: TableEntry[];
givenTheFollowingUsersExist(table: TableEntry[]) {
this.#table = table;
}
thenTheUserSShouldHaveTheTwitterHandleS(username: string, twitter: string) {
const user = this.#table.find((user) => user.name === username);
expect(user!.twitter).toBe(twitter);
}
}
Please not that TableEntry typing is suggested directly as table parameter type, but for simplicity it has been extracted by a simple IDE refactor to a type alias.
Scenario Outlines
Scenario outlines are automatically converted to multiple tests:
Feature: Scenario Outline
Scenario Outline: Eating cucumbers
Given there are <start> cucumbers
When I eat <eat> cucumbers
Then I should have <left> cucumbers
Examples:
| start | eat | left |
| 12 | 5 | 7 |
| 20 | 5 | 15 |
class CucumberSteps {
#count = 0;
givenThereAreNCucumbers(count: number) {
this.#count = count;
}
whenIEatNCucumbers(eaten: number) {
this.#count -= eaten;
}
thenIShouldHaveNCucumbers(left: number) {
expect(this.#count).toBe(left);
}
}
Please note that the CucumberSteps class is the same as the one used in the Numbers section. Variables are automatically replaced inside the sentences.
With Scenario Outlines, instead of creating one test, it creates one test per example. And it also includes the variable values in the test name:
Eating cucumbers — 12, 5, 7
Eating cucumbers — 20, 5, 15
Using Other Step Definitions
In large projects, often one single class implementing all the step definitions is not enough. And it also not recommended to have a new class for each feature file. But, we can create a collection of own step definitions classes and use them in multiple feature files.
Using multiple step definitions
You can pass multiple step definitions to the wish
function:
import { wish } from "gherkin-genie";
import { AppleSteps } from "./AppleSteps";
import { OrangeSteps } from "./OrangeSteps";
import { FruitSteps } from "./FruitSteps";
wish("./Fruits.feature", [AppleSteps, OrangeSteps, FruitSteps]);
In this case, the step definitions are merged together.
Wishing other step definitions
You can get the step definitions instances by using the get
function.
It allows you to use them in other steps.
For example, given:
// "AppleSteps.ts"
export class AppleSteps {
#count: number;
getCount() {
return this.#count;
}
givenIHaveNApples(apples: number) {
this.#count = apples;
}
}
Assuming that OrangeSteps
is similar to AppleSteps
,
FruitSteps
can use them as follows:
// "FruitSteps.ts"
import { get } from "gherkin-genie";
import { AppleSteps } from "./AppleSteps";
import { OrangeSteps } from "./OrangeSteps";
class FruitSteps {
#appleSteps: AppleSteps;
#orangeSteps: OrangeSteps;
constructor() {
this.#appleSteps = wish(AppleSteps);
this.#orangeSteps = wish(OrangeSteps);
}
thenIShouldHaveNFruits(fruits: number) {
const appleCount = this.#appleSteps.getCount();
const orangeCount = this.#orangeSteps.getCount();
expect(appleCount + orangeCount).toBe(fruits);
}
}
Auto-injecting other step definitions
You do not need to give all the step definitions to the feature test creations, they are added when wishing for them.
Although in the first example we have manually injected several
step definitions classes into the wish
function,
it is possible to auto-inject them.
When the wish
function is called,
it will instance all the step definitions obtained by the get
function,
and add them to the list of step definitions.
Feature: Fruits
Scenario: We can mix several fruits
Given I have 3 apples
And I have 2 oranges
Then I should have 5 fruits
import { wish } from "gherkin-genie";
import { FruitSteps } from "./FruitSteps";
wish("./Fruits.feature", [FruitSteps]);
Because FruitSteps uses the get
function to obtain
the AppleSteps
and OrangeSteps
instances,
their step definitions are automatically injected into the test.
Wishing for other helper instances
You can wish for the singleton instance of any class once you test have been started.
The difference between those instance and the steps instances is that these are short liven, they only live inside each test (Scenario).
Feature: Apples
Scenario: I can count how many apples we have
Given I have 3 apples
And you have 5 apples
Then we should have 8 apples
import { wish } from "gherkin-genie";
class AppleCounter {
#count: number = 0;
add(apples: number) {
this.#count += apples;
}
getCount() {
return this.#count;
}
}
class AppleSteps {
#appleCounter: AppleCounter;
beforeEach() {
this.#appleCounter = wish(AppleCounter);
}
givenIHaveNApples(apples: number) {
this.#appleCounter.add(apples);
}
givenYouHaveNApples(apples: number) {
this.#appleCounter.add(apples);
}
thenWeShouldHaveNApples(fruits: number) {
expect(this.#appleCounter.getCount()).toBe(fruits);
}
}
wish("./WishInstances.feature", [AppleSteps]);
The wish for step restriction
All wishes for step instances outside the constructors will be considered test helper instances and its steps will not be activated.
So for example, given the example of fruits:
Feature: Fruits
Scenario: We can mix several fruits
Given I have 3 apples
And I have 2 oranges
Then I should have 5 fruits
This code will make it fail:
// ❌ WRONG "FruitSteps.ts"
import { wish } from "gherkin-genie";
import { AppleSteps } from "./AppleSteps";
import { OrangeSteps } from "./OrangeSteps";
class FruitSteps {
thenIShouldHaveNFruits(fruits: number) {
const appleCount = wish(AppleSteps).getCount();
const orangeCount = wish(OrangeSteps).getCount();
expect(appleCount + orangeCount).toBe(fruits);
}
}
This will throw an error while creating tests because, unless we inject in the wish array AppleSteps and OrangeSteps, the steps for apples and oranges will be missing.
Configuration
It is possible configure the function test
to be used to create the tests.
Custom test runners
By default, Gherkin Genie uses the global test
to create the tests.
If your test runner does not work by default,
because it uses a different name for the test function,
or it expects you to import it,
you can configure it by passing a function to the wish({ testFn })
function:
import test from "ava";
import { wish } from "gherkin-genie";
wish({ testFn: test });
Demo
Feature file:
Feature: Magic of Disappearing Cucumbers
Scenario: Eating 5 out of 12 cucumbers
Given I have 12 cucumbers
When I eat 5 cucumbers
Then I should have 7 cucumbers remaining
Initial test:
import { wish } from "gherkin-genie";
wish("./Demo.feature");
Error message:
FAIL demo/Demo.spec.ts
● Test suite failed to run
There are missing steps. Please implement them:
class WishedSteps {
givenIHaveNCucumbers(number1: number) {
throw new Error("Unimplemented");
}
whenIEatNCucumbers(number1: number) {
throw new Error("Unimplemented");
}
thenIShouldHaveNCucumbersRemaining(number1: number) {
throw new Error("Unimplemented");
}
}
73 | if (!missingSteps.length) return;
74 |
> 75 | throw new Error(
| ^
76 | [
77 | "There are missing steps. Please implement them:",
78 | "",
at verifySteps (src/createFeatureTests.js:75:9)
at createFeatureTests (src/createFeatureTests.js:21:3)
at wish (src/wish.js:21:3)
at Object.<anonymous> (demo/Demo.spec.ts:19:23)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 0.16 s, estimated 1 s
Copy paste and fix implementation:
import { wish } from "gherkin-genie";
class CucumberSteps {
#count = 0;
givenIHaveNCucumbers(count: number) {
this.#count = count;
}
whenIEatNCucumbers(eaten: number) {
this.#count -= eaten;
}
thenIShouldHaveNCucumbersRemaining(left: number) {
expect(this.#count).toBe(left);
}
}
wish("./Demo.feature", [CucumberSteps]);
✨ Done!