{"id":15685548,"url":"https://github.com/nfour/fermenter","last_synced_at":"2025-05-07T08:27:31.575Z","repository":{"id":32634110,"uuid":"135972200","full_name":"nfour/fermenter","owner":"nfour","description":"A strongly typed Gherkin test runner","archived":false,"fork":false,"pushed_at":"2023-01-04T21:42:08.000Z","size":1434,"stargazers_count":9,"open_issues_count":13,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-16T00:13:25.064Z","etag":null,"topics":["cucumber","gherkin","jest","mocha","tests","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nfour.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-06-04T04:52:07.000Z","updated_at":"2023-02-02T01:07:41.000Z","dependencies_parsed_at":"2023-01-14T21:47:09.749Z","dependency_job_id":null,"html_url":"https://github.com/nfour/fermenter","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Ffermenter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Ffermenter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Ffermenter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Ffermenter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nfour","download_url":"https://codeload.github.com/nfour/fermenter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252841606,"owners_count":21812507,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cucumber","gherkin","jest","mocha","tests","typescript"],"created_at":"2024-10-03T17:26:31.218Z","updated_at":"2025-05-07T08:27:31.553Z","avatar_url":"https://github.com/nfour.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![CircleCI](https://img.shields.io/circleci/project/github/nfour/fermenter.svg?style=flat-square)![npm](https://img.shields.io/npm/v/fermenter.svg?style=flat-square)![npm](https://img.shields.io/npm/dt/fermenter.svg?style=flat-square)\n\n# Fermenter\n\nFermenter is a tool for running tests written in the english-like language **Gherkin**.\n\nIt aims to be a **function programming** alternative to **[CucumberJS](https://github.com/cucumber/cucumber-js)** by doing away with things like global state and auto-discovery.\n\nYou use the same test runner you are used to: **Jest**, **Mocha**, **Ava**.\n\nYou also use the same expression parsers as CucumberJS:\n- Gherkin language: [docs.cucumber.io/gherkin/reference](https://docs.cucumber.io/gherkin/reference)\n- Cucumber Expressions: [docs.cucumber.io/cucumber/cucumber-expressions](https://docs.cucumber.io/cucumber/cucumber-expressions/)\n\n-----------------------\n\n+ [Examples](#examples)\n  + [Advanced example](#advanced-example)\n+ [Api](#api)\n  + [Scenarios and skipping steps](#scenarios-and-skipping-steps)\n  + [Background](#background)\n  + [Tables](#tables)\n  + [Scenario Outlines](#scenario-outlines)\n  + [Typescript tips](#typescript-tips)\n  + [Global hooks](#global-hooks)\n  + [Using other test runners](#using-other-test-runners)\n+ [How it works](#how-it-works)\n  + [Coming from CucumberJS](#coming-from-cucumberjs)\n  + [How it runs](#how-it-runs)\n+ [More info](#more-info)\n\n\n## Examples\n\nBelow is a very minimal example:\n\n```ts\nimport { Feature } from 'fermenter';\nimport { sum } from 'lodash';\n\nFeature('./features/calculator.feature', ({ Scenario }) =\u003e {\n  Scenario('A simple addition test')\n    .Given('I have numbers {int} and {int}', (state, num1, num2) =\u003e [num1, num2])\n    .When('I add the numbers', sum)\n    .Then('I get {int}', (summed, expectedResult) =\u003e {\n      expect(summed).toBe(expectedResult);\n    });\n});\n```\n\nIn the above example `state` is strongly typed throughout each step.\n\nBelow is a more realistic use of `state` with object spreading:\n\n```ts\nimport { Feature } from 'fermenter';\nimport { sum } from 'lodash';\n\nFeature('./features/calculator.feature', ({ Scenario }) =\u003e {\n  Scenario('A simple addition test')\n    .Given('I have numbers {int} and {int}', (state: {} = {}, num1, num2) =\u003e {\n      return { ...state, numbers: [num1, num2] };\n    })\n    .When('I add the numbers', (state) =\u003e {\n      return { ...state, summed: sum(state.numbers) };\n    })\n    .Then('I get {int}', ({ summed, numbers }, expectedResult) =\u003e {\n      expect(numbers.length).toBe(2);\n      expect(summed).toBe(expectedResult);\n    });\n});\n```\n\n\nThe above test is explicitly mapped to the feature file`'./features/calculator.feature'`:\n\n```gherkin\nFeature: Calculator\n  Scenario: A simple addition test\n    Given I have numbers 3 and 4\n    When I add the numbers\n    Then I get 7\n```\n\nTo run this test we simply use our test runner (in this case **Jest**) as normal:\n\n```bash\n$\u003e yarn jest\n\n PASS  src/tests/calculator.test.ts\n  Feature: Calculator\n    Scenario: A simple addition test\n      ✓ Given: I have numbers 3 and 4\n      ✓ When: I add the numbers\n      ✓ Then: I get 7\n```\n\nThe above output is generated by the default **Jest** runner, and thus will include any debug, diff, snapshotting and the error stack traces you expect.\n\n### Advanced example\n\nBelow is a more advanced example [calculator.test.ts](src/tests/calculator.test.ts):\n\n```ts\nimport { delay } from 'bluebird';\nimport { Feature } from 'fermenter';\nimport { getNumbers, addNumbers, checkResult, multiplyNumbers } from './steps';\n\nFeature('./features/calculator.feature', ({ Scenario, Background, ScenarioOutline, AfterAll }) =\u003e {\n  Background()\n    .Given('I can calculate', () =\u003e {\n      expect(Math).toBeTruthy();\n    });\n\n  Scenario('A simple addition test')\n    .Given('I have numbers {int} and {int}', getNumbers)\n    .When('I add the numbers', addNumbers)\n    .Then.skip('I get {int}', checkResult) // This scenario step is skipped\n    .And('I get {int}') // This scenario step is skipped because its missing a callback\n\n  // This scenario and its steps will be skipped\n  Scenario.skip('A simple multiplication test')\n    .Given('I have numbers {int} and {int}', getNumbers)\n    .When('I multiply the numbers', multiplyNumbers)\n    .Then('I get {int}', checkResult);\n\n  ScenarioOutline('A simple subtraction test')\n    .Given('I have numbers {int} and {int}', getNumbers)\n    .When.skip('I subtract the numbers', subtractNumbers)\n    .Then('I get {int}', async (state, expectedResult) =\u003e { // Functions can be async too!\n      await delay(5000);\n\n      expect(state.result).toBe(expectedResult);\n    });\n\n  AfterAll(() =\u003e {\n    console.log('Done!')\n  })\n});\n```\n\nThe above example maps to: [calculator.feature](src/tests/features/calculator.feature).\n\n## Api\n\nThe project bundles TypeScript definitions and so the library api is easy to discover.\n\nFor more examples, see the tests: [src/tests](src/tests)\n\n### Scenarios and skipping steps\n\n```ts\nFeature('./test.feature', ({ Scenario }) =\u003e {\n  Scenario('The scenario')\n    .Given('I can do stuff')\n\n  Scenario.skip('The scenario') // Scenarios can be referred to more than once\n    .Given('I can do stuff') // All steps are skipped due to .skip\n\n  Scenario('Another scenario')\n    .Then('I can do stuff') // Skipped: the callback is missing\n    .And.skip('I can do stuff') // Skipped: because of .skip\n    .And('I can do stuff', () =\u003e {}) // Not skipped!\n})\n```\n\nEach of the above **Scenario** is executed with its own state initial state.\n\nThe state provided to the **Scenario** defaults to `{}`, an empty object unless specified otherwise by a **Background**. More on that later.\n\nYou may also choose one scenario to run with `only`:\n\n```ts\nFeature('./test.feature', ({ Scenario }) =\u003e {\n  Scenario('The scenario')\n    .Given('I can do stuff', () =\u003e {}) // Skipped!\n\n  Scenario.only('The scenario')\n    .Given('I can do stuff', () =\u003e {}) // Not skipped!\n\n  Scenario('Another scenario')\n    .Given('I can do stuff', () =\u003e {}) // Skipped!\n})\n```\n\nSkipping behaviour:\n- When a **individual step** is skipped:\n  - The skipped function is replaced with `(v) =\u003e v`. A state **passthrough** function.\n  - Steps after it are **NOT** skipped.\n- When an entire **Scenario** is skipped:\n  - **All steps** are skipped\n\n### Background\n\n**Backgrounds** are used to define common state/preparation between scenarios.\n\nBuilding on top of the previous example, lets add a background to supply some initial state to the **Scenario**.\n\n```ts\nimport { Feature } from 'fermenter';\nimport { sum } from 'lodash';\n\nFeature('./features/calculator.feature', ({ Scenario, Background }) =\u003e {\n  Background()\n    .Given('I start with {int}', (state, initialNum) =\u003e initialNum)\n\n  /** We have to tell TypeScript what the background return type is */\n  Scenario\u003cnumber\u003e('A simple addition test')\n    .Given('I have numbers {int} and {int}', (initialNum, num1, num2) =\u003e [initialNum, num1, num2])\n    .When('I add the numbers', sum)\n    .Then('I get {int}', (summed, expectedResult) =\u003e {\n      expect(summed).toBe(expectedResult);\n    });\n});\n```\n\nSome things to note:\n- **Backgrounds** do not require a `name`\n  - `Background()` will match all **Backgrounds** in the feature file.\n  - `Background('My background')` will match only `'My background'`\n- **Backgrounds** only support `Given()` and `Given().And().And()` steps\n- The state returned from the last step of a **Background** will supply all **Scenarios** in the **Feature**\n- **Scenario** can be provided an initial state generic\n\n\n### Tables\n\nTo use tables defined in your Gherkin, do this:\n\n```ts\nimport { Feature, ITable } from 'fermenter';\n\nFeature('./features/calculator.feature', ({ Scenario, }) =\u003e {\n  Scenario('A simple addition test')\n    .Given('I have the following numbers:', (state = {}, table: IGherkinTableParam) =\u003e {\n      const [{ a, b }] = table.rows.mapByTop();\n\n      return {\n        ...state,\n        a: parseInt(a, 10),\n        b: parseInt(b, 10),\n      };\n    });\n});\n```\n\n- See the `ITable` type for details and examples:\n  - [src/types/parser.ts#L115](src/types/parser.ts#L115)\n- There are also some tests here: [src/lib/\\_\\_tests\\_\\_/GherkinTableReader.spec.ts](src/lib/__tests__/GherkinTableReader.spec.ts)\n\n### Scenario Outlines\n\n**Scenario Outlines** function just like **Scenarios**, but are run for each provided example in the `.feature`.\n\n```ts\nFeature('./test.feature', ({ ScenarioOutline }) =\u003e {\n  ScenarioOutline('My outline')\n    .When('foo is {string}')\n});\n```\n\nSee the **Scenario** section for more info.\n\n\n### Typescript tips\n\nThe above is a simple example. Your **Background** state type will likely be quite large and you shouldnt have to manually define a type! We can use features from TS 3.0 to help here, and some types included in **Fermenter**.\n\nThis time, lets infer the type of the background:\n\n```ts\nimport { Feature, AsyncReturnType } from 'fermenter';\nimport { delay } from 'bluebird';\nimport { sum } from 'lodash';\n\n/** Lets also make this function async! */\nasync function initialNumberStep (state: undefined, initialNum: number) {\n  await delay(500);\n\n  return initialNum;\n};\n\nFeature('./features/calculator.feature', ({ Scenario, Background }) =\u003e {\n  Background()\n    .Given('I start with {int}', initialNumberStep)\n\n  /** We have to tell TypeScript what the background return type is */\n  Scenario\u003cAsyncReturnType\u003ctypeof initialNumberStep\u003e\u003e('A simple addition test')\n    .Given('I have numbers {int} and {int}', (initialNum, num1, num2) =\u003e [initialNum, num1, num2])\n    .When('I add the numbers', sum)\n    .Then('I get {int}', (summed, expectedResult) =\u003e {\n      expect(summed).toBe(expectedResult);\n    });\n});\n```\n\nYay, we didn't have to define any plumbing types!\n\nNotes:\n- The first **Scenario**'s **Given** will be run after the **Background** is complete\n- Using `AsyncReturnType` allows one to retrieve the promisified (or not) return value of any function\n\n\n### Global hooks\n\nYou may utilize this global hook to instrument or alter your steps and their state:\n\n```ts\nimport { globallyBeforeEachStep } from 'fermenter';\n\ngloballyBeforeEachStep((step, state) =\u003e {\n  console.log({\n    stepName: step.name,\n    scenarioName: step.definition.name,\n    featureName: step.definition.feature.name,\n    incomingState: state,\n  });\n\n  return state; // You can change this\n});\n```\n\n### Using other test runners\n\nTo set your own test runner, pass its test methods when configuring a feature:\n\n```ts\nFeature({\n  feature: '...',\n  methods: { test, afterAll, beforeAll, describe }\n}, () =\u003e {})\n\n// or\n\n/** Here we wrap `Feature` and give it the global variables mocha provides as test methods */\nexport const MochaFeature = (...args: Parameters\u003ctypeof Feature\u003e) =\u003e\n  Feature(\n    { methods: { test, describe, afterAll: after, beforeAll: before  }, ...args[0] },\n    args[1]\n  )\n```\n\nThe framework has been tested in **Mocha**, **Jest** and **Cypress** but is expected to work with any which satisfy the test runner method interfaces.\n\n\n## How it works\n\n### Coming from CucumberJS\n\nIf you're coming from **CucumberJS** then some functionality is carried over:\n- Same gherkin parser\n- Same expression parser\n\n### How it runs\n\n- **Scenarios** and **ScenarioOutlines** are executed with fresh `state`\n  - **There is no `this`**\n  - `state` is reduced with each step function\n  - **Strong TypeScript support** for step `state`\n    - Steps will inherit the `state` type of the previous step return value\n    - You shouldn't need to manually define types for step functions used inline\n- **Step names are no longer restricted to be unique** for every feature file.\n  - To reuse a step, simply reuse the function itself\n- **Tests serve as a composition root**. No magic happens inside this library.\n- Tests are executed by your test runner, which defaults to **Jest**\n- Steps are executed in **synchronous** order.\n- **Background** steps are executed before **Scenario** steps\n- **Features** can be run asynchronously depending on your runner (as they are file-separated)\n  - Each **Scenario** will also be run **synchronously** after another for a given `Feature()` definition\n    - This is the default in **Jest**\n  - It is possible to define multiple `Feature()` calls to the same `.feature` file within many `.test.ts` files, which can allow the same feature to be run in parallel inside **Jest** for example.\n\n--------------------\n\n## More info\n\n- [CONTRIBUTING.md](./CONTRIBUTING.md)\n- [CHANGELOG.md](./CHANGELOG.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnfour%2Ffermenter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnfour%2Ffermenter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnfour%2Ffermenter/lists"}