Skip to content

QuickPickle is a plugin for Vitest to run tests written in Gherkin Syntax. It seamlessly integrates Gherkin Feature files into your Vitest testing workflow by parsing them with the official Gherkin Parser and running them as Vitest tests.

License

Notifications You must be signed in to change notification settings

dnotes/quickpickle

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QuickPickle

QuickPickle is a plugin for Vitest to run tests written in Gherkin Syntax. It seamlessly integrates Gherkin Feature files into your Vitest testing workflow by parsing them with the official Gherkin Parser and running them as Vitest tests.

Features

  • Seamless integration of Gherkin Feature files into Vitest testing workflow
  • Full support for Gherkin 6, using the official Gherkin Parser
  • Full ESM, typescript and javascript support (because it's Vitest)
  • Vitest-native test extensions concurrent, sequential, skip, todo, and fails
  • Multiple test environments with vitest.workspace configurations
  • Custom world constructors, similar to CucumberJS

CucumberJS Feature List

Benefits of Gherkin

  • Write tests in plain English (or your team's language)

    Because it uses natural language, Gherkin Syntax enables all stakeholders to understand and contribute to tests, and having a common vocabulary to describe functionality makes it easy to agree on and verify what the program does.

  • Reuse test step definitions to minimize test code

    Bugs can happen even in test code, so it's best if the test code changes as seldom as possible, but functionality changes all the time. A small library of reusable step definitions is much easier to maintain than a large corpus of test code.

Goals of QuickPickle

  1. Support the main technical ideas behind Gherkin / Cucumber

    • Natural language for features
    • Discrete, re-usable step definitions
  2. Make Gherkin tests easy to set up and use in Javascript projects

    • Run tests with Vitest, to avoid painful snafus from js/ts/esm/commonjs issues.
    • Provide a plugin for testing web applications. (completed with packages/playwright)
    • Provide a plugin for testing components. (in progress with packages/components)
  3. Experiment with means of supporting open-source projects

    • Provide a Svelte component exposing Gherkin steps, so that end users can potentially write bug reports and feature requests in the form of Gherkin scenarios. (in progress with packages/svelte)
    • Provide a GitHub actions and/or bots that interface with AI to:
      • write Gherkin scenarios based on issue reports
      • write code based on Gherkin scenarios
  4. Experiment with Gherkin for unsanctioned testing purposes

    Note: These ideas do NOT align with Cucumber best practices; use at your own risk (of being ridiculed on reddit, etc.)

    • Develop a library of common step definitions that could be used across projects to test UI interaction: Gherkin scenarios and steps should generally be written with domain-specific language instead of focusing on the user interface (see the "Lots of user interface details" section of [Cucumber Anti-Patterns]). However:

      1. The browser is a specific domain that is nonetheless common across all web projects; most will need an "I navigate to {string}" step.
      2. There may be a benefit to having a well-written set of Gherkin steps for UI testing that is re-usable across a wide range of projects.
      3. This would make it really easy for people to get started, even without writing any step definitions.

      (in progress with packages/playwright)

    • Develop a library of common step definitions that could be used across projects for "unit testing" components: Gherkin is meant for testing the behavior of an application, not the code, and in most cases it is not a good tool for unit testing. However:

      1. In some ways components represent units of behavior, often containing minimal code.
      2. It might be beneficial to have a library of well-written Gherkin steps for testing every type of interaction that it supports.

      (in progress with packages/components)

Installation

npm install --save-dev quickpickle vitest

Configuration

To get QuickPickle working you'll need to do three things:

  1. configure Vitest to use QuickPickle, in vite.config.ts or vitest.workspace.ts
  2. create a step definition file to
    • import or create your step definitions
    • set a different world variable constructor (optional)
  3. install & configure VSCode Cucumber plugin (optional but highly recommended)

QuickPickle configuration in Vitest

Add the quickpickle plugin to your Vitest configuration in vite.config.ts (or .js, etc.). Also add the configuration to get the feature files, step definitions, world file, etc.

// vite.config.ts
import { quickpickle } from 'quickpickle';

export default {
  plugins: [
    quickpickle() // <-- Add the quickpickle plugin
  ],
  test: {
    include : [
      'features/*.feature', // <-- Add Gherkin feature files into "test" configuration
      // (you'll probably want other test files too, for unit tests etc.)
    ],
    setupFiles: ['./tests/tests.steps.ts'] // <-- specify each setupfile here
  },
};

If you have multiple configurations, you can also add the test settings in vitest.workspace.ts. This is also where you could set up separate configurations for components vs. application, different browser environments, different world constructors, etc.

// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';

export default defineWorkspace([
  { // configuration for feature files testing the application
    extends: './vite.config.ts',
    test: {
      include : [ 'tests/*.feature' ],
      setupFiles: [ 'tests/tests.steps.ts' ],
    },
  },
  { // a second configuration for feature files testing components
    extends: './vite.config.ts',
    test: {
      include : [ 'src/lib/*.feature' ],
      setupFiles: [ 'tests/components.steps.ts' ],
    },
  },
  { // configuration for unit tests
    test: {
      include : [ 'tests/*.test.ts' ],
    }
  }
])

Step definition file

You'll always need a step definition file, to set up the step defintions and potentially the world variable constructor. Here is an exmaple if you want to use @quickpickle/playwright to test web sites:

// tests/example.steps.ts
import '@quickpickle/playwright/actions' // <-- import action step definitions from @quickpickle/playwright
import '@quickpickle/playwright/outcomes' // <-- import outcome step definitions from @quickpickle/playwright

import '@quickpickle/playwright/world' // <-- use the playwright world variable (optional)

import { Given, When, Then } from 'quickpickle' // <-- the functions to write step definitions

// Custom step definitions
Given('a/another number {int}', async (world) => {
  if (!world.numbers) world.numbers = []
  world.numbers.push(int)
})

Cucumber plugin for VSCode

If you use VSCode, you'll want a cucumber plugin for code completion when writing gherkin features. Try the official "Cucumber" plugin, by "Cucumber". You'll also need to configure it so that it sees your step definitions.

// VSCode settings.json
"cucumber.glue": [
    "**/*.steps.{ts,js,mjs}",
    "**/steps/*.{ts,js,mjs}"
],

Usage

Write Feature files

Write your feature files in the directory specified above. Common convention is to place feature files in a "features" directory, though some prefer the "tests" directory. You can put them anywhere as long as they're listed in the "include" configuration in vite.config.ts.

# features/example.feature
Feature: A simple example

  Scenario: Adding numbers
    Given a number 1
    And a number 2
    And a number 3
    And another number 3
    Then the sum should be 9

Run tests

npx vitest --run

Write step definitions

Write your step definitions in a typescript or javascript file as configured in the "setupFiles" declaration in your vitest config.

These files will be imported into the Vitest test runner, and the code for Given, When, Then will register each of the step definitions with quickpickle. These step definitions should run immediately, i.e. at the top level of the script, not as exported functions like a normal node module would do.

// features/example.steps.ts
import { Given, Then } from 'quickpickle'

Given('a/another number {int}', (world, int) => {
  if (!world.numbers) world.numbers = [int]
  else world.numbers.push(int)
})

Then('the sum should be {int}', (world, int) => {
  expect(world.numbers.reduce((a,b) => a + b, 0)).toBe(int)
})

Define a custom "world" variable constructor

To define a custom world variable constructor in QuickPickle, you can use the setWorldConstructor function exported from the package. This allows you to create a custom World class that extends the QuickPickleWorld interface, enabling you to add your own properties and methods to the world object. By setting up a custom world constructor, you can initialize specific data or services that will be available to all your step definitions throughout the test execution.

Each Scenario will receive a new instance of the world variable based on this class. If you need to write asynchronous code, you can write it inside an "init" function. Here is an example that should set up a sqlite database and initiate it with a "users" table:

import { setWorldConstructor, QuickPickleWorld, QuickPickleWorldInterface } from 'quickpickle'
import sqlite3 from 'sqlite3'
import { Database, open } from 'sqlite'

class CustomWorld extends QuickPickleWorld {
  db?: Database;

  constructor(context: TestContext, info?: QuickPickleWorldInterface['info']) {
    super(context, info)
  }

  async init() {
    await super.init()
    this.db = await this.setupDatabase()
  }

  private async setupDatabase(): Promise<Database> {
    const db = await open({
      filename: ':memory:',
      driver: sqlite3.Database
    })

    await db.exec(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)`)

    return db
  }
}

setWorldConstructor(CustomWorld)

For a real world example of a custom world constructor, see PlaywrightWorld.ts.

Run Gherkin tests in your CI workflow

If Vite is properly configured, Gherkin tests should run the same on local environments as it does in your CI workflow. If you use browser testing tools like Playwright, you may need to take a step to set it up first.

See quickpickle's release.yml for an example.

Differences from CucumberJS

The main library for Gherkin in the JS ecosystem is CucumberJS, a test runner written explicitly for Gherkin tests. QuickPickle aims to be a complete replacement for that library, using Vite to handle bundling and test running while maintaining functional parity with the original. Nonetheless, there are differences. Here are the important ones that have come to notice:

world replaces this in step definitions

Each step definition in QuickPickle receives a "world" variable as its first parameter.

// QuickPickle step definition
Given('a number {int}', function(world:QuickPickleWorldInterface, int:number) {
  if (!Array.isArray(world.numbers)) world.numbers = [int]
  else world.numbers.push(int)
})

In CucumberJS, you would write your step definitions using "this":

// CucumberJS step definition
Given('a number {int}', function(int:number) {
  if (!Array.isArray(this.numbers)) this.numbers = [int]
  else this.numbers.push(int)
})

Aside from the fact that a passed variable is much easier to think about for a compiler than custom bindings, this led to some sub-optimal usage in modern Javascript, including:

  • Arrow functions couldn't be used in step definitions or hooks, or this wouldn't work.
  • When using a custom world, you would have to add (this:CustomWorldType, ...params) in typescript files or else you wouldn't get the right types.

Passing a variable is clearer and more intuitive, and provides more reliable support for modern JS.

Current step info in world variable

In CucumberJS, the default world variable contains information about the test suite, but not the current step. In QuickPickle, the "world" variable passed to each test step contains an "info" property with the data about the Scenario.

export interface QuickPickleWorldInterface {
  info: {
    config: QuickPickleConfig   // the configuration for QuickPickle
    feature: string             // the Feature name (not file name)
    scenario: string            // the Scenario name
    tags: string[]              // the tags for the Scenario, including tags for the Feature and/or Rule
    steps: string[]             // an array of all Steps in the current Scenario
    stepIdx?: number            // the index of the current Step, starting from 1 (not 0)
    rule?: string               // the Rule name, if any
    step?: string               // the current Step
    line?: number               // the line number, in the file, of the current Step
    explodedIdx?: number        // the index of the test case, if exploded, starting from 1 (not 0)
    errors: any[]               // an array of errors that have occurred, if the Scenario is tagged for soft failure
  }
  context: TestContext,         // the Vitest context
  isComplete: boolean           // (read only) whether the Scenario is on the last step
  config: QuickPickleConfig                       // (read only) configuration for QuickPickle
  worldConfig: QuickPickleConfig['worldConfig']   // (read only) configuration for the World
  common: {[key: string]: any}                    // Common data shared across tests --- USE SPARINGLY
  init: () => Promise<void>                       // function called by QuickPickle when the world is created
  tagsMatch(tags: string[]): string[]|null        // function to check if the Scenario tags match the given tags
}

Special Tags

In Gherkin, the meanings of tags are determined by the implementation, there are no defaults. Since quickpickle uses Vitest, some tags have been given default meanings:

  • @todo / @wip: Marks scenarios as "todo" using Vitest's test.todo implementation
  • @skip: Skips scenarios using Vitest's test.skip implementation
  • @fails / @failing: Ensures that a scenario fails using Vitest's test.fails implementation
  • @concurrent: Runs scenarios in parallel using Vitest's test.concurrent implementation
  • @sequential: Runs scenarios sequentially using Vitest's test.sequential implementation

The relevant tags can be configured. Plugins may also have default tag implementations; for example, @quickpickle/playwright has @nojs to disable javascript, and @chromium, @firefox, and @webkit to run a scenario on a particular browser.

Unsupported CucumberJS Features

  • return "skipped" in step definitions or hooks to skip a step definition or hook (use world.context.skip())
  • setDefaultTimeout (use testTimeout in Vitest config)
  • setDefinitionFunctionWrapper (use Before and After hooks)
  • setParallelCanAssign (use @concurrent and @sequential special tags)
  • attachments with world.attach, world.log, and world.link not supported, use Vitest reporters
  • dry run not supported, but you can use vitest list which is similar
  • [custom snippet formats] (only async/await javascript is supported)
  • filtering CucumberJS tag matching with "and", "not", etc. In Vitest it is only possible to match the name of the test. With QuickPickle tests the name does include the tags, so running vitest -t "@some-tag" does work, but you can't specify that it should run all tests without a certain tag.

Acknowledgements

This project started out as a fork of vitest-cucumber-plugin by Sam Ziegler. It's been almost completely rewritten in the following ways:

  • it has been converted to typescript
  • a custom Gherkin parser has been replaced with the official Gherkin Parser
  • the step definition format has been reverted to more closely match CucumberJS

Nonetheless, the brilliant ideas behind the original plugin are still present in the architecture of this project. Thanks Sam, your work blew my mind.

"it's so simple!" - Owen Wilson from Zoolander peers over an early 2000s iMac computer, a mad glint in his eye.

Media and Tutorials

  • Behavioral testing with Gherkin and SvelteKit: The Svelte Summit presentation from 19 October 2024.

    • What is Gherkin / Cucumber and why should you use it
    • Comparing Gherkin to straight Playwright code
    • Testing the Svelte "Sverdle" app that ships with the demo site
    • Adding a new feature using Behavior Driven Development assisted by AI
  • QuickPickle dev vlog 27 Oct. 2024: A near-real-time exploration of QuickPickle for beginners, highlighting Playwright functionality.

    • How to set up QuickPickle for testing websites with Playwright
    • Testing with and without Javascript, in multiple browsers, at multiple resolutions
    • Using QuickPickle's explodeTags to minimize test verbiage
  • QuickPickle dev vlog 19 Nov. 2024: Migrating a complex CucumberJS implementation to QuickPickle

    tl;dr:

    • Set the proper configuration for vite / vitest
    • Change imports
    • Find and replace "this" to "world" in step definitions and hooks
    • Move your "setDefaultTimeout" to the vitest config "testTimeout"
    • Make any necessary changes in your custom world constructor, if applicable

About

QuickPickle is a plugin for Vitest to run tests written in Gherkin Syntax. It seamlessly integrates Gherkin Feature files into your Vitest testing workflow by parsing them with the official Gherkin Parser and running them as Vitest tests.

Resources

License

Stars

Watchers

Forks

Packages

No packages published