Skip to content

Commit

Permalink
extend support for 'rule' sections in features
Browse files Browse the repository at this point in the history
  • Loading branch information
Markus Mueller committed Mar 18, 2021
1 parent 06cf438 commit 88faaeb
Show file tree
Hide file tree
Showing 14 changed files with 423 additions and 99 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Feature: Vending machine

Rule: Dispenses items if correct amount of money is inserted

Scenario: Selecting a snack
Given the vending machine has "Maltesers" in stock
And I have inserted the correct amount of money
When I select "Maltesers"
Then my "Maltesers" should be dispensed

Scenario Outline: Selecting a beverage
Given the vending machine has "<beverage>" in stock
And I have inserted the correct amount of money
When I select "<beverage>"
Then my "<beverage>" should be dispensed

Examples:
| beverage |
| Cola |
| Ginger ale |

Rule: Returns my money if item is out of stock

Scenario: Selecting a snack
Given the vending machine has no "Maltesers" in stock
And I have inserted the correct amount of money
When I select "Maltesers"
Then my money should be returned

Scenario: Selecting a beverage
Given the vending machine has no "Cola" in stock
And I have inserted the correct amount of money
When I select "Cola"
Then my money should be returned
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Feature: Vending machine

Rule: Dispenses items if correct amount of money is inserted

Scenario: Selecting a snack
Given the vending machine has "Maltesers" in stock
And I have inserted the correct amount of money
When I select "Maltesers"
Then my "Maltesers" should be dispensed

Scenario Outline: Selecting a beverage
Given the vending machine has "<beverage>" in stock
And I have inserted the correct amount of money
When I select "<beverage>"
Then my "<beverage>" should be dispensed

Examples:
| beverage |
| Cola |
| Ginger ale |

Rule: Returns my money if item is out of stock

Scenario: Selecting a snack
Given the vending machine has no "Maltesers" in stock
And I have inserted the correct amount of money
When I select "Maltesers"
Then my money should be returned

Scenario: Selecting a beverage
Given the vending machine has no "Cola" in stock
And I have inserted the correct amount of money
When I select "Cola"
Then my money should be returned
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { StepDefinitions, loadFeature, autoBindStepsWithRules } from '../../../../src';
import { VendingMachine } from '../../src/vending-machine';

export const vendingMachineSteps: StepDefinitions = ({ given, and, when, then }) => {
let vendingMachine: VendingMachine;

const myMoney = 0.50;

given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => {
vendingMachine = new VendingMachine();
vendingMachine.stockItem(itemName, 1);
});

given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => {
vendingMachine = new VendingMachine();
vendingMachine.stockItem(itemName, 0);
});

and('I have inserted the correct amount of money', () => {
vendingMachine.insertMoney(myMoney);
});

when(/^I select "(.*)"$/, (itemName: string) => {
vendingMachine.dispenseItem(itemName);
});

then(/^my money should be returned$/, () => {
const returnedMoney = vendingMachine.moneyReturnSlot;
expect(returnedMoney).toBe(myMoney);
});

then(/^my "(.*)" should be dispensed$/, (itemName: string) => {
const inventoryAmount = vendingMachine.items[itemName];
expect(inventoryAmount).toBe(0);
});
};

const feature = loadFeature('./examples/typescript/specs/features/extended-rules-auto-step-binding.feature', {collapseRules: false});

autoBindStepsWithRules([feature], [ vendingMachineSteps ]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { loadFeature, defineRuleBasedFeature } from '../../../../src';
import { DefineStepFunction } from '../../../../src/feature-definition-creation';
import { VendingMachine } from '../../src/vending-machine';

const feature = loadFeature('./examples/typescript/specs/features/extended-rules-definition.feature', {collapseRules: false});

defineRuleBasedFeature(feature, (rule) => {
let vendingMachine: VendingMachine;

const myMoney = 0.50;

const givenTheVendingMachineHasXInStock = (given: DefineStepFunction) => {
given(/^the vending machine has "([^"]*)" in stock$/, (itemName: string) => {
vendingMachine = new VendingMachine();
vendingMachine.stockItem(itemName, 1);
});
};

const givenTheVendingMachineHasNoXInStock = (given: DefineStepFunction) => {
given(/^the vending machine has no "([^"]*)" in stock$/, (itemName: string) => {
vendingMachine = new VendingMachine();
vendingMachine.stockItem(itemName, 0);
});
}

const givenIHaveInsertedTheCorrectAmountOfMoney = (given: DefineStepFunction) => {
given('I have inserted the correct amount of money', () => {
vendingMachine.insertMoney(myMoney);
});
};

const whenISelectX = (when: DefineStepFunction) => {
when(/^I select "(.*)"$/, (itemName: string) => {
vendingMachine.dispenseItem(itemName);
});
};

const thenXShouldBeDespensed = (then: DefineStepFunction) => {
then(/^my "(.*)" should be dispensed$/, (itemName: string) => {
const inventoryAmount = vendingMachine.items[itemName];
expect(inventoryAmount).toBe(0);
});
}

const thenMyMoneyShouldBeReturned = (then: DefineStepFunction) => {
then(/^my money should be returned$/, () => {
const returnedMoney = vendingMachine.moneyReturnSlot;
expect(returnedMoney).toBe(myMoney);
});
}

rule("Dispenses items if correct amount of money is inserted", (test) => {

test('Selecting a snack', ({ given, and, when, then }) => {
givenTheVendingMachineHasXInStock(given);
givenIHaveInsertedTheCorrectAmountOfMoney(given);
whenISelectX(when);
thenXShouldBeDespensed(then);
});

test('Selecting a beverage', ({ given, and, when, then }) => {
givenTheVendingMachineHasXInStock(given);
givenIHaveInsertedTheCorrectAmountOfMoney(given);
whenISelectX(when);
thenXShouldBeDespensed(then);
});
});

rule("Returns my money if item is out of stock", (test) => {

test('Selecting a snack', ({ given, and, when, then }) => {
givenTheVendingMachineHasNoXInStock(given);
givenIHaveInsertedTheCorrectAmountOfMoney(given);
whenISelectX(when);
thenMyMoneyShouldBeReturned(then);
});

test('Selecting a beverage', ({ given, and, when, then }) => {
givenTheVendingMachineHasNoXInStock(given);
givenIHaveInsertedTheCorrectAmountOfMoney(given);
whenISelectX(when);
thenMyMoneyShouldBeReturned(then);
});
});
});
9 changes: 7 additions & 2 deletions examples/typescript/src/vending-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const ITEM_COST = 0.50;
export class VendingMachine {
public balance: number = 0;
public items: { [itemName: string]: number } = {};
public moneyReturnSlot: number = 0;

public stockItem(itemName: string, count: number) {
this.items[itemName] = this.items[itemName] || 0;
Expand All @@ -14,10 +15,14 @@ export class VendingMachine {
}

public dispenseItem(itemName: string) {
if(this.items[itemName] === 0) {
this.moneyReturnSlot = this.balance;
this.balance = 0;
}

if (this.balance >= ITEM_COST && this.items[itemName] > 0) {
this.balance -= ITEM_COST;
this.items[itemName]--;
}

this.items[itemName]--;
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 78 additions & 38 deletions src/automatic-step-binding.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ParsedFeature, ParsedScenario, ParsedScenarioOutline } from './models';
import { ParsedFeature, ScenarioGroup } from './models';
import { matchSteps } from './validation/step-definition-validation';
import { StepsDefinitionCallbackFunction, defineFeature } from './feature-definition-creation';
import {
StepsDefinitionCallbackFunction,
defineFeature,
defineRuleBasedFeature,
DefineScenarioFunctionWithAliases
} from './feature-definition-creation';
import { generateStepCode } from './code-generation/step-generation';

const globalSteps: Array<{ stepMatcher: string | RegExp, stepFunction: () => any }> = [];
Expand All @@ -9,49 +14,84 @@ const registerStep = (stepMatcher: string | RegExp, stepFunction: () => any) =>
globalSteps.push({ stepMatcher, stepFunction });
};

export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => {
stepDefinitions.forEach((stepDefinitionCallback) => {
stepDefinitionCallback({
defineStep: registerStep,
given: registerStep,
when: registerStep,
then: registerStep,
and: registerStep,
but: registerStep,
pending: () => {
// Nothing to do
},
const registerSteps = (stepDefinitionCallback: StepsDefinitionCallbackFunction) => {
stepDefinitionCallback({
defineStep: registerStep,
given: registerStep,
when: registerStep,
then: registerStep,
and: registerStep,
but: registerStep,
pending: () => {
// Nothing to do
}
});
};

const matchAndDefineSteps = (group: ScenarioGroup, test: DefineScenarioFunctionWithAliases, errors: string[]) => {
const scenarioOutlineScenarios = group.scenarioOutlines.map((scenarioOutline) => scenarioOutline.scenarios[0]);

const scenarios = [ ...group.scenarios, ...scenarioOutlineScenarios ];

scenarios.forEach((scenario) => {
test(scenario.title, (options) => {
scenario.steps.forEach((step, stepIndex) => {
const matches = globalSteps.filter((globalStep) => matchSteps(step.stepText, globalStep.stepMatcher));

if (matches.length === 1) {
const match = matches[0];

options.defineStep(match.stepMatcher, match.stepFunction);
} else if (matches.length === 0) {
const stepCode = generateStepCode(scenario.steps, stepIndex, false);
// tslint:disable-next-line:max-line-length
errors.push(
`No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${group.title}". Please add the following step code: \n\n${stepCode}`
);
} else {
const matchingCode = matches.map(
(match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}`
);
errors.push(
`${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${group.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join(
'\n\n'
)}`
);
}
});
});
});
};

export const autoBindSteps = (features: ParsedFeature[], stepDefinitions: StepsDefinitionCallbackFunction[]) => {
stepDefinitions.forEach(registerSteps);

const errors: string[] = [];

features.forEach((feature) => {
defineFeature(feature, (test) => {
const scenarioOutlineScenarios = feature.scenarioOutlines
.map((scenarioOutline) => scenarioOutline.scenarios[0]);

const scenarios = [...feature.scenarios, ...scenarioOutlineScenarios];

scenarios.forEach((scenario) => {
test(scenario.title, (options) => {
scenario.steps.forEach((step, stepIndex) => {
const matches = globalSteps
.filter((globalStep) => matchSteps(step.stepText, globalStep.stepMatcher));

if (matches.length === 1) {
const match = matches[0];

options.defineStep(match.stepMatcher, match.stepFunction);
} else if (matches.length === 0) {
const stepCode = generateStepCode(scenario.steps, stepIndex, false);
// tslint:disable-next-line:max-line-length
errors.push(`No matching step found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Please add the following step code: \n\n${stepCode}`);
} else {
const matchingCode = matches.map((match) => `${match.stepMatcher.toString()}\n\n${match.stepFunction.toString()}`);
errors.push(`${matches.length} step definition matches were found for step "${step.stepText}" in scenario "${scenario.title}" in feature "${feature.title}". Each step can only have one matching step definition. The following step definition matches were found:\n\n${matchingCode.join('\n\n')}`);
}
});
matchAndDefineSteps(feature, test, errors);
});
});

if (errors.length) {
throw new Error(errors.join('\n\n'));
}
};

export const autoBindStepsWithRules = (
features: ParsedFeature[],
stepDefinitions: StepsDefinitionCallbackFunction[]
) => {
stepDefinitions.forEach(registerSteps);

const errors: string[] = [];

features.forEach((feature) => {
defineRuleBasedFeature(feature, (ruleDefinition) => {
feature.rules.forEach((rule) => {
ruleDefinition(rule.title, (test) => {
matchAndDefineSteps(rule, test, errors);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const defaultErrorSettings = {
const defaultConfiguration: Options = {
tagFilter: undefined,
scenarioNameTemplate: undefined,
collapseRules: true,
errors: defaultErrorSettings,
};

Expand Down

0 comments on commit 88faaeb

Please sign in to comment.