Cucumber Expressions
Cucumber Expressions are a templating format to create dynamic step definitions, without the burden of Regular Expressions.
A Cucumber Expression is essentially a string which matches a step
defined in a gherkin .feature
file.
They could match literally:
- Gherkin
- Expression
Given I have navigated to my profile
import { Given } from "@autometa/runner";
Given("I have navigated to my profile", () => {
// ...
});
Or they could match against an expression variable:
- Gherkin
- Expression
Given I have 4 dogs
import { Given } from "@autometa/runner";
Given("I have {int} dogs", (dogCount) => {
// ...
});
When a step definition contains an expression variable (or several), the corresponding value is extracted from the Gherkin step, and passed as an argument to the step definition function.
In this example, dogCount
will be 4
. For the following standard expression
formats, the type of the variable will be inferred and an explicit type annotdation (dogCount: number
) is not required:
{int}
{float}
{string}
{word}
Additionally, the following custom types are supported out of the box:
{number}
- accepts integer or float numbers
{boolean}
- accepts
true
orfalse
. Does not require quotes around the value
- accepts
{bool}
- alias for
{boolean}
- alias for
{date}
- Converts a literate date string, or human readable date string like
'1 hour from now'
or'tomorrow'
to aDate
object with that value - Requires the value to be wrapped in quotes
- Converts a literate date string, or human readable date string like
{any}
- wildcard
{unknown}
- type enforced wildcard
{primitive}
- Attempts to convert the value to a primitive type
- string, number, boolean, or date
Defining Custom Types
Custom types can be defined with the defineParameterType
function. This function
accepts a collection of ParameterType objects with the following structure:
{
name: string;
regex: RegExp;
transformer?: (arg: string) => any;
useForSnippets?: boolean;
preferForRegexpMatch?: boolean;
pattern?: string;
typeName?: string;
}
Which represent the following details:
name
- the name of the type. This is used to reference the type in the expressionregexp
- a regular expression to match against the value in the expressionprimitive
- the primitive value the result represents, if any. I.e for a primitiveNumber
and a string'1'
, the result would be the parsed number1
type
- the type Constructor the result represents, if any. I.e for a typeDate
and a string'1 hour from now'
, the result would be aDate
object with the value of 1 hour from now- Accepts any class constructor as long as it accepts a single string or primitive (see below) argument
- If a
primitive
is also defined, the value will first be converted to the specified primitive type before being passed to the constructor
transform
- a function to transform the value from the expression to the desired type- If a
primitive
is also defined, the value will first be converted to the specified primitive type before being passed to the transformer - If a
type
is also defined, the value will first be converted to the specified type before being passed to the transformer - If both a
primitive
andtype
are defined, the value will first be converted to the specified primitive type, then passed to the constructor and finally the constructed object will be passed to the transformer
- If a
Example
With transform
import { defineParameterType } from "@autometa/runner";
import { getUserByUsername } from "./user-service";
defineParameterType({
name: "user",
regex: /@([a-zA-Z]+)/,
transformer: (username) => {
return getUserByUsername(username);
}
});
With primitive
import { defineParameterType } from "@autometa/runner";
defineParameterType({
name: "int",
regex: /[+-]?\d+/,
primitive: Number
});
With type
- Defining Parameters
- dtos.ts
import { defineParameterType } from "@autometa/runner";
import { MyDto } from "./dtos";
defineParameterType({
name: "myDto",
regex: /@([a-zA-Z]+)/,
type: MyDto
});
// dtos.ts
export class MyDto {
constructor(public name: string) {}
age: number;
}
Declaring Custom Types
By default, any custom types you define cannot be inferred in step definitions and will be typed
as unknown
, which must be handled inside the step.
Given("I have a {myDto}", (myDto) => {
myDtoFunction(myDto); // Error, expected MyDto but was unknown
// myDto is typed as unknown
const casted = myDto as MyDto;
myDtoFunction(casted)
}
However it is possible to override the @autometa/scopes
module in a declaration file with your own
custom types, which will be used to infer the type of the step definition.
For example, if you have a custom type MyDto
defined in dtos.ts
:
// typings/app.d.ts
import type { MyDto } from "./dtos";
declare module "@autometa/scopes" {
export interface CustomTypes {
myDto: MyDto;
}
}
Next, tell TypeScript about your custom types by adding the following to your tsconfig.json
:
{
"compilerOptions": {
"types": ["typings/app.d.ts"]
}
}
Then in your step definition, the type of the myDto
parameter will be inferred as MyDto
:
import { Given } from "@autometa/runner";
Given("I have a {myDto}", (myDto) => {
// myDto is inferred as MyDto
});
Accessing the App In Expressions
It is possible to access the running tests App
context from expressions,
in the transformer. The app will be passed as the last-most value pased to the transformer
arguments.
For example an expression can be configured to create a builder, assign it to the world, and in future steps retrieve it.
- Cucumber Expression
- Gherkin Step
- Step Definition
defineParameterType({
name: "builder:widget",
regex: [/"([^"]*)"/, /'([^']*)'/],
transform: (value: string, app: App) => {
const key = `$builder_${value}`;
if(!app.world[key]){
app.world[key]= new FooBuilder().name(value);
}
return app.world[key]
}
})
Scenario: my foo scenario
Given a widget 'User Creator'
* the 'User Creator' widget domain is 'Admin'
* the 'User Creator' widget cache is 'Dynamic'
When I execute the 'User Creator' widget
Given('a widget {builder:widget}', Pass)
Given(
'the {builder:widget} widget {word} is {string}',
(widgetBuilder, propertyName, propertyValue) => {
widgetBuilder.assign(propertyName, propertyValue)
}
)
When(
'I execute the {builder:widget} widget',
(widgetBuilder, app) => {
const widget = widgetBuilder.build();
await app.widgetClient.post(widget);
}
)