Pickled cucumber is a Gherkin implementation with several condiments.
npm i pickled-cucumber
const setup = require('pickled-cucumber').default; // note: `.default` here!
const options = {
// opt-in into aliases, entities, documentation, etc.
};
const fn = ({ Given, When, Then }) => {
// define your steps here
};
setup(fn, options);
In the most basic usage all you need to do is define cucumber steps. Besides the
Given
, When
and Then
above you also have some other options,
check here.
To run tests you just need to execute:
node ./node_modules/.bin/cucumber-js -r path/to/file/above.js
This will run all your .feature
files in the features/
directory. You can
also pass files and directories to the cucumber-js
binary to control exactly
which tests should be executed.
pickled-cucumber
has serveral modules you can opt-in during setup by specifying
different configurations in the options
argument to setup
. In this section
we'll go through these modules one by one.
The core module is what you get for free just by using pickled-cucumber
without any additional configuration.
The core module includes:
- Variables and context
- Built-in aliases
- Comparison functions and built-in operators
- Tear down
Variables are variables :). Variables have scenario scope (i.e. they are not shared across scenarios). This scenario scope is called the context. We'll cover how to manipulate the context in this section in a moment.
As with regular variables in most programming languages there are two things you want to do with variables: assign them a value, and read the value from the variable.
To assign a value to a variable you can use the built-in cucumber step:
Given variable {variable} is (.+)`
This assigns whatever is after the is
to the variable named {variable}
.
For example:
Given variable A is "hello, world"
As with most steps in pickled-cucumber
you have two variants of the same step:
one with an inline value (the one above), and one that accepts the value in a
docstring in the next line(s). For example:
Given variable Person is
"""
{
"first": "John",
"last": "Smith"
}
"""
Alternatively, you can programatically assign a value to a variable inside a step
definition, using the setCtx
function. For example:
const fn = ({ setCtx, Given }) => {
Given('my name is (.+)', name => setCtx('N', name));
};
setup(fn);
With this definition, both steps below do the same thing:
Given my name is John
Given variable N is "John"
Assigning a variable programmatically can be useful to store the result of
processing a When
step. Later, the result can be tested using Then
steps.
Now here is where things get really interesting: variables can be interpolated
into any steps in pickled-cucumber
in a similar fashion as how strings
are interpolated in JavaScript.
For example:
Given variable Name is "John"
Then say hi to ${Name}
Then say hi to John
Both Then
steps above execute the same code.
Here's a more complex example:
Given variable P is
"""
{
"first": "John",
"last": "Smith"
}
"""
Then say hi to ${P.first} ${P.last}
Then say hi to John Smith
And a more realistic example:
Given a user U
When GET /api/profiles/${U.id}
...more stuff here...
The cool part about automatic variable interpolation is that you do not need to do anything special while writing your steps to make it work.
As with setCtx
, you can also programatically read the value of a variable
using the getCtx
function.
For example:
const fn = ({ getCtx, Then }) => {
Then('the user (.+) was deleted from the DB', async varName => {
const user = getCtx(varName);
// assert that `user` was deleted from the DB, something like:
// assert(await doesNotExist(user.id));
});
};
setup(fn);
Let's pick up from the last example:
const fn = ({ getCtx, Then }) => {
Then(
'the user (.+) was deleted from the DB',
async varName => { },
});
};
setup(fn);
The step text is a string that represents a regular expression (hence the
(.+)
). While this is very flexible, it can be quite hard to understand.
To address this issue, pickled-cucumber
comes with built-in aliases that make
step definitions a lot easier to read.
The step above could be rewritten to be:
Then(
'the user {variable} was deleted from the DB',
async varName => { },
});
These two ways of defining the step are functionally equivalent.
The built-in aliases include:
{boolean}: true|false
{int}: \d+
{op}: contain|contains|exists|has keys|include|includes|is|matches|starts with|at [\w.\[\]\$\{\}-]+ (?:contain|contains|exists|has keys|include|includes|is|matches|starts with)
{variable}: \w+
{variables}: \w+(?:,\s*\w+|\s+and\s+\w+)*
{word}: \S+
For now ignore {op}
, we'll get back to that one in the next section. Also know
that you can define custom aliases that operate exactly like the built-in ones.
More on that in a bit.
One key aspect of assertion (i.e. Then
) steps is that you'll need to compare
some expected value given to the step with some actual value presumably
generated or somewhat affected by a When
step.
pickled-cucmber
comes with two things that makes writing these Then
assertion steps super easy: the {op}
alias and the compare
function.
Here's an example:
const fn = ({ compare, Then }) => {
Then(
'the user with id {int} {op} (.*)',
async (id, op, payload) => {
const user = await getUserById(id);
compare(op, user, payload);
},
});
};
setup(fn);
Then the user with id 1 is { "id": 1, "name": "John" }
Then the user with id 1 at name is "John"
Then the user with id 1 at name contains "Jo"
Then the user with id 1 includes { "name": "John" }
The built-in operators include:
a contain b: checks that the string representation of 'a' contains 'b'
a contains b: checks that the string representation of 'a' contains 'b'
a exists any: checks that 'a' is truthy
a has keys b: checks that the object 'a' has all the keys in array 'b'
a include b: checks that the array or object 'a' contains the partial 'b'
a includes b: checks that the array or object 'a' contains the partial 'b'
a is b: checks that 'a' deep equals 'b'
a matches b: checks that the string representation of 'a' matches regex 'b'
a starts with b: checks that the string representation of 'a' starts with 'b'
Additionally, the at
operator allows you to compare a (deeply nested) property
of actual
. The syntax for at
is: at {path}
where path is a property name
a series of property names separated by dots (e.g. address.zipCode
).
If a key of the actual
object happens to contain one or more .
s you can
quote the at
path segment with "
to target the element. For example, the 1
in { "a": { "b.c": 1 } }
can be targeted with a."b.c"
.
Let's say that you have a Given a user {variable}
step that creates a user in
your database (DB) and stores the user in the variable. After a test finishes, it would
be polite for your step to delete that user from the DB to keep things tidy.
You can accomplish this by registering tear-down logic using the onTearDown
function.
For example:
const fn = ({ onTearDown, setCtx, Given }) => {
Given(
'a user {variable}',
async (varName) => {
const user = generateRandomUser();
await insertUser(user);
onTearDown(() => deleteUser(user.id));
},
});
};
setup(fn);
The usage module prints reference documentation of all steps, operators and aliases available for writing tests.
To enable the usage module just specify usage: true
in the setup options.
For example:
const options = {
usage: true,
};
const fn = (args) => {
// define your steps here using args
}
setup(fn, options);
This produces something like:
Step reference
--------------
Given variable {variable} is
Given variable {variable} is (.+)
Operators
---------
a contain b: checks that the string representation of 'a' contains 'b'
a contains b: checks that the string representation of 'a' contains 'b'
a exists any: checks that 'a' is truthy
a has keys b: checks that the object 'a' has all the keys in array 'b'
a include b: checks that the array or object 'a' contains the partial 'b'
a includes b: checks that the array or object 'a' contains the partial 'b'
a is b: checks that 'a' deep equals 'b'
a matches b: checks that the string representation of 'a' matches regex 'b'
a starts with b: checks that the string representation of 'a' starts with 'b'
Aliases
-------
{boolean}: true|false
{int}: \d+
{op}: contain|contains|exists|has keys|include|includes|is|matches|starts with|at [\w.\[\]\$\{\}-]+ (?:contain|contains|exists|has keys|include|includes|is|matches|starts with)
{variable}: \w+
{variables}: \w+(?:,\s*\w+|\s+and\s+\w+)*
{word}: \S+
0 scenarios
0 steps
The debug module stops the test execution when the first step fails and drops you into a debugging console. In there you can run arbitrary test steps and inspect the test context.
To enable the debug module just specify debug: true
in the setup options.
It might be a good idea to link this setting to an environment variable so that
you can control when to drop into the debugging console (i.e. you most
definitely don't want this happening in CI or in long test runs).
For example:
const options = {
debug: true,
};
const fn = (args) => {
// define your steps here using args
}
setup(fn, options);
When dropped into the debugging console, just type help
to see available
commands.
Note that you can also run arbitrary test steps in the console and you can use
the steps
command to see all available steps.
Also note that you have <Tab>
completion in the console so make sure you use
it (hitting <Tab><Tab>
in the console will show you everything you can do!).
debug> help
The alias module allows you to (re-)define regular expression aliases that you can use to define steps with less clutter.
To enable the alias module just specify aliases
in the setup options.
For example:
const options = {
aliases: {
'proper-name': /[A-Z][a-z]+/
// Note: this a mapping from string to regexp
},
};
const fn = ({ When }) => {
// Note: when using the alias you wrap it in `{}` instead of `()`.
When('I say hi to {proper-name}', (name) => { });
}
setup(fn, options);
The inititial context module allows you to inject some variables in the context of every test.
To enable the initial context module just specify initialContext
in the
setup options. initialContext
is a function that returns the initial context.
The function will be called once for each scenario (in the Before
hook).
For example:
const options = {
initialContext: () => ({
random: getSomeFancyRandomString(),
now: Date.now(),
})
};
const fn = (args) => {
// define your steps here using args
}
setup(fn, options);
And in your tests:
When creating a user with email "${random}@email.com"
Then the user at creationDate is ${now}
The entities module allows you to test data persistence in your apps.
For example, consider the following test:
Given a user U with { "id": 1, "name": "John" }
When PATCH /api/users/1 with payload { "name": "Ringo" }
Then the document for user U at name is "Ringo"
Assuming that the When
step was defined somehow (e.g. by the http module),
defining a user
entity gives you both Given
and Then
steps above and
many more.
To enable the entities module just specify entities
in the setup options.
For example:
const options = {
entities: {
user: someUserEntityDefinition,
},
};
const fn = (args) => {
// define your steps here using args
}
setup(fn, options);
Because there is a lot more to this module, we have more detailed documentation in a separate place, see:
TBD
TBD
To test your local copy run npm run pack
and install the package like this:
npm install ../pickled-cucumber/dist/pickled-cucumber-<version>.tgz