Skip to content

midasum/vitest-bdd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vitest-bdd

Gherkin test runner for vitest.

The goal is to provide a simple and intuitive way to write tests for your application. Use with vitest and Gherkin extensions for VS-Code.

vitest-bdd banner

You can also write tests in Markdown with Gherkin code blocks for Spec Driven Development:

image

Tests can run in parallel (no shared state) and are fast and hot reloadable.

Features

  • write with Gherkin, execute with vitest !
  • Gherkin block inside markdown
  • ReScript step definitions and full bindings for vitest
  • async tests
  • concurrent testing
  • failed tests in steps definitions and Gherkin
  • supports number, string and table parameters
  • steps are explicitly linked to your context (easy to trace usage)
  • supports "Background"
  • ESM and CJS projects support
  • Gherkin parsing with @cucumber/gherkin.

Usage

Install

pnpm i --save-dev vitest-bdd

Setup

Create a vitest config file

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vitestBdd } from 'vitest-bdd'

export default defineConfig({
  plugins: [vitestBdd()],
  test: {
    include: ['**/*.feature', '**/*.spec.ts', '**/*.mdx']
  }
})

Options

Options are passed as an object to the vitestBdd function. The default options are:

{
  debug: false,
  concurrent: true,
  markdownExtensions: [".md", ".mdx", ".markdown"],
  gherkinExtensions: [".feature"],
  stepsResolver: stepsResolver,
}

And the default stepsResolver function is below. This resulver would find the following files for a "./test/foobar.feature" file:

  • ./test/foobar.feature.ts
  • ./test/foobar.feature.js
  • ... etc
  • ./test/foobar.steps.ts
  • ./test/foobar.steps.js
  • ... etc
  • ./test/foobarSteps.ts
  • ./test/foobarSteps.js
  • ... etc

The last setting helps for ReScript users to mach ./test/Foobar.feature with ./test/FoobarSteps.res steps files.

function baseResolver(path: string): string | null {
  for (const ext of ['.ts', '.js', '.mjs', '.cjs', '.res.mjs']) {
    const p = `${path}${ext}`
    if (existsSync(p)) {
      return p
    }
  }
  return null
}

export function stepsResolver(path: string): string | null {
  // Resolves to a specific steps file
  // from /foo/bar.feature
  // to   /foo/bar.feature[.ts|.js|...]
  // or   /foo/bar.steps[.ts|.js|...]
  // or   /foo/barSteps[.ts|.js|...]
  for (const r of ['.feature', '.steps', 'Steps']) {
    const p = baseResolver(path.replace(/\.feature$/, r))
    if (p) {
      return p
    }
  }
  // Resolves to a common steps file in the directory:
  // from /foo/bar.feature
  // to   /foo/steps[.ts|.js|...]
  return baseResolver(join(basename(path), 'steps'))
}
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vitestBdd } from 'vitest-bdd'

export default defineConfig({
  plugins: [vitestBdd({ markdownExtensions: ['.mdx', '.text'] })],
  test: {
    include: ['**/*.feature', '**/*.spec.ts', '**/*.mdx', '**/*.text']
  }
})

Describe your features

Define your feature in a file with the .feature extension:

# src/domain/test/calculator.feature
Feature: Calculator

  Scenario: Add two numbers
    Given I have a "basic" calculator
    When I add 1 and 2
    Then the result is 3

  Scenario: Advanced calculator
    Given I have an "rpn" calculator
    When I enter 1
    And I enter 2
    And I divide
    Then the result is 0.5

Create a steps file with .feature.ts extension (and exact same name as the feature file):

// src/domain/test/calculator.feature.ts
import { type Signal } from 'tilia'
import { expect } from 'vitest'
import { Given, type Step } from 'vitest-bdd'
import { makeCalculator } from '../feature/calculator'

// You can reuse steps in multiple contexts
// Here anything that has a result value.
function resultAssertions(Then: Step, calculator: { result: Signal<number> }) {
  // We define an async step, just to look cool 😎.
  Then('the result is {number}', async (n: number) => {
    await calculator.proccessBigComputation()
    expect(calculator.result.value).toBe(n)
  })
}

// You can use any variable name instead of When, And, and Then to match the
// language of the Gherkin messages, such as { Quand, Alors, Et }, etc. We show
// the code in an async situation (because it's the most difficult to handle).
// The last parameter is the test context (vitest.TestContext).
Given(
  'I have a {string} calculator',
  async ({ When, And, Then }, mode, _testContext) => {
    switch (mode) {
      case 'basic': {
        const calculator = basicCalculator()
        When('I add {number} and {number}', calculator.add)
        And('I subtract {number} and {number}', calculator.subtract)
        And('I multiply {number} by {number}', calculator.multiply)
        And('I divide {number} by {number}', calculator.divide)
        resultAssertions(Then, calculator)
        break
      }
      case 'rpn': {
        const calculator = rpnCalculator()
        When('I enter {number}', calculator.enter)
        And('I enter {number}', calculator.enter)
        And('I divide', calculator.divide)
        resultAssertions(Then, calculator)
        break
      }
      default:
        throw new Error(`Unknown calculator type "${type}"`)
    }
  }
)

For ReScript, the bindings are a little bit simpler without the possibility to rename the "step" function.

Please note that due to limitations with the type system, we cannot have multiple arguments for "Given" in rescript. You need to add a step for extra parameters or use a table.

// src/domain/test/CalculatorSteps.res
open VitestBdd

// To show assertion reuse (could be just added in the given block).
let resultAssertions = ({ step }, calculator) => {
  // We define an async step, just to look cool 😎 (again).
  step("the result is {number}", async (n: number) => {
    await calculator.proccessBigComputation()
    expect(calculator.result.value).toBe(n)
  })
}

given("I have a {string} calculator", async ({ step }, ctype) => {
  switch ctype {
  | "basic" => {
    let calculator = basicCalculator()
    step("I add {number} and {number}", calculator.add)
    step("I subtract {number} and {number}", calculator.subtract)
    step("I multiply {number} by {number}", calculator.multiply)
    step("I divide {number} by {number}", calculator.divide)
    resultAssertions(step, calculator)
  }
  | "rpn": {
    let calculator = rpnCalculator()
    step("I enter {number}", calculator.enter)
    step("I enter {number}", calculator.enter)
    step("I divide", calculator.divide)
    resultAssertions(step, calculator)
  }
})

Reuse steps

You can reuse steps in multiple contexts. For example, a preference manager could implement the interface Form (to access and set values) and you can reuse the form steps:

// src/domain/test/preference-manager.feature.ts
import { formSteps } from '@steps/form'

Given('I have a preference manager', ({ Step }) => {
  const preferenceManager = makePreferenceManager()
  formSteps(Step, preferenceManager)
})

Gherkin in Markdown

vitest-bdd parses the gherkin code fences and compiles them into a test suite.

Example: src/domain/test/some.md

### Basic calculator

```gherkin
Feature: Calculator in md

Background:
  Given I have a "basic" calculator
  Then the title is "basic"
```

Some other markdown that does nothing.

## Basic operations

```gherkin
Scenario: Add two numbers
  When I add 1 and 2
  Then the result is 4

Scenario: Advanced calculator
  When I divide 1 by 2
  Then the result is 0.5
```

And this is some more markdown.

Define steps in src/domain/test/some.md.ts

Gherkin Table

src/domain/test/tabular.feature

Feature: Table

  Background:
    Given I have a table
      | firstName | lastName | isActive |
      | Charlie   | Smith    | true     |
      | Bob       | Johnson  | false    |
      | Alice     | Williams | true     |

  Scenario: Sort by name
    When I sort by "lastName"
    Then the table is
      | firstName | lastName | isActive |
      | Bob       | Johnson  | false    |
      | Charlie   | Smith    | true     |
      | Alice     | Williams | true     |
# etc
// src/domain/test/tabular.feature.ts
import { Given, Then, When, toRecords } from 'vitest-bdd'
import { makeTable } from '../feature/table'

Given('I have a table', ({ When, Then }, data) => {
  // data : User[]
  const table = makeTable(data)

  When('I sort by {string}', table.sort)
  Then('the table is', data => {
    expect(table.list).toEqual(toRecords(data))
  })
})

You can also use toNumbers or toStrings to convert the first column of a table to a list of numbers or strings.

Non-english support

You can write your tests in any language supported by Cucumber (around 40).

# language: fr
# /some/feature/calculator.feature
# language: fr
Fonctionnalité: Calculatrice

  Scénario: Addition de deux nombres
    Soit une calculatrice
    Quand j'ajoute 15 et 10
    Alors le rĂ©sultat doit ĂȘtre 25

  Scénario: Addition de nombres négatifs
    Soit une calculatrice
    Quand j'ajoute -15 et -10
    Alors le rĂ©sultat doit ĂȘtre -25

  Scénario: Soustraction de deux nombres
    Soit une calculatrice
    Quand je soustrais 5 Ă  12
    Alors le rĂ©sultat doit ĂȘtre 7

And the steps file:

// /some/feature/calculator.feature.ts
import { expect } from 'vitest'
import { Given } from 'vitest-bdd'
import { makeCalculator } from '../feature/calculator'

Soit('un calculator', ({ Quand, Alors }) => {
  const calculator = makeCalculator()
  Quand("j'ajoute {number} et {number}", calculator.add)
  Quand('je soustrais {number} Ă  {number}', calculator.subtract)

  Alors('le rĂ©sultat doit ĂȘtre {number}', (expected: string) => {
    expect(calculator.result.value).toBe(expected)
  })
})

Don't forget to update some vscode settings (if you use the cucumber autocomplete extension for VS Code):

// .vscode/settings.json
{
  "workbench.iconTheme": "diagonal-architecture-light-icon-theme",
  "cucumberautocomplete.steps": [
    "*/src/domain/test/**/*.feature.ts",
    "*/src/domain/test/**/*Steps.res" // This is for ReScript
  ],
  "cucumberautocomplete.formatConfOverride": {
    "Fonctionnalité": 0,
    "Scénario": 1,
    "Soit": 2,
    "Quand": 2,
    "Alors": 2
  },
  "cucumberautocomplete.strictGherkinCompletion": true,
  "cucumberautocomplete.smartSnippets": true,
  "cucumberautocomplete.syncfeatures": "src/domain/test/**/*.feature"
}

And finally, here are some nice extensions for VS Code that can support your BDD journey:

{
  "recommendations": [
    "midasum.diagonal-architecture",
    "alexkrechik.cucumberautocomplete"
  ]
}

Changelog

  • 0.6.0 (2025-09-01)
    • Add support for table parsing (toRecords, toNumbers, toStrings)
    • Add concurrent option (true by default)
    • Add test context as last parameter to given step
    • Add full bindings for vitest assertions in ReScript
    • Fix bundling to not include external dependencies (this created issues with concurrency and testContext)
  • 0.5.1 (2025-08-28)
    • Remove support for arrays in tests (accidental breaking change)
  • 0.5.0 (2025-08-27)
    • Add support for arrays in tests
  • 0.4.0 (2025-08-17)
    • Add support for ReScript unit tests (with source maps!)
  • 0.3.0 (2025-07-26)
    • Add options for markdown extension (and default support for .mdx)
  • 0.2.0 (2025-07-23)
    • Add support for Gherkin code blocks in markdown
    • Add basic support for ReScript step definitions
    • (remove experimental Gherkin in markdown support)
  • 0.1.0 (2025-07-04)
    • Add async support
    • Add concurrency support
    • Fixed negative number parsing
    • Added support for scientific number notation
    • Create basic plugin

About

simple vitest plugin for Gherkin tests

Resources

Stars

Watchers

Forks

Packages

No packages published