Library / SDK
codegram/spinach avatar
codegram/spinach

Spinach: Gherkin specifications whose steps are plain Ruby methods

Spinach is a BDD framework on top of Gherkin.

577 stars64 forksRubyMIT

At a glance

What is it?
Spinach is a BDD framework for Ruby that reads the same Gherkin feature files Cucumber does, but binds each feature to a class and each step to a method on it. The payoff is encapsulation and reusability; the cost is that the ecosystem around it has been quiet for a while.
Who is it for?
Adopt Spinach if your team already writes Ruby, wants acceptance tests in Gherkin, and values keeping step logic in ordinary classes and modules over Cucumber's world step registry. Do not adopt it if you need a framework with a current ecosystem, a large plugin set, or active maintenance, because the only tagged release is 0.12.0 from 2023-02-09 and the last push was on 2026-05-02.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 175 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The same Gherkin files, a different binding model

Spinach is a high-level BDD framework that uses the expressive Gherkin language, the one used by Cucumber, to help you define executable specifications of your application or library's acceptance criteria.

The files on disk are the same ones Cucumber would read. A feature file is a Feature with a description and one or more Scenarios, and each Scenario has Given, When, Then and And steps. If you are moving from Cucumber, the specifications transfer unchanged, and the README even shows a Gherkin block in a cucumber language tag.

What changes is the binding. In Spinach, features map to their own classes and their steps are just methods of that class. That single design choice is the reason the project exists, and it is stated as the first of the design goals: step maintainability, because the steps are methods and the class is a normal Ruby class, which encourages step encapsulation. The second goal is step reusability, and in that case you wrap the steps in plain old Ruby modules.

The project was conceived as an alternative to Cucumber, and the design goals read like a list of what the author wanted to do differently rather than a list of features. The homepage is codegram.github.com/spinach, the gem is on RubyGems as spinach, the licence is MIT, and the primary language is Ruby.

The generator turns a feature file into a class

You start by adding spinach to the Gemfile in your test group:

ruby
group :test do
  gem 'spinach'
  # gem 'rspec'
end

Then you create a features folder in your app or library and write your first feature. The README's example is a feature named Test how spinach works, with a Formal greeting scenario and an Informal greeting scenario, both of which append names to an empty array and pass it to a method.

The steps file is where Spinach differs from what you would expect. Remember that in Spinach steps are just Ruby classes, following a camelcase naming convention, and the Spinach generator does the scaffolding for you:

shell
$ spinach --generate

Given a feature named Test how spinach works, the generator produces a class called Spinach::Features::TestHowSpinachWorks, which inherits from Spinach::FeatureSteps, with one empty step method per step in the feature. The class name is derived from the feature name in camelcase, and the file lands at features/steps/test_how_spinach_works.rb, with the name in snake case.

That is the whole loop: write a feature, run the generator, fill in the empty methods, run spinach again and watch the scenarios turn green. There is no separate step definition language, and no registry of step patterns to match, which is the design's whole appeal for a Ruby shop.

Because the steps are a class, private methods and mixins work

The filled in example in the README is worth reading closely, because it shows what you can do that a pure step definition file makes awkward.

Each step method sets an instance variable on the class, so the Given step creates an empty array into @array, the And step appends two names to it, the When step captures the output of the code under test into @output using a capture_output helper, and the Then steps assert on that output. The assertion style is the older minitest style, with @output.must_include, rather than an RSpec expect.

The helper is a private method on the class, and the README points this out: it is just a class, you can use private methods, mix in modules or whatever. The capture_output helper redirects $stdout and $stderr into a StringIO, yields, and restores the originals, which is the kind of small piece of plumbing that in a step definition file would have to live somewhere global.

The example also shows the object under test living in a plain Ruby module at the bottom of the same file, a Greeter module with a self.greet method that branches on the length of the name array. The application code is not mocked or stubbed at all here, it is just called.

So the practical shape of a Spinach feature is a small class with a public method per step, a private section for helpers, and the code under test exercised in between. That is closer to how you would write the test in plain Ruby than to how Cucumber treats a step file.

Shared steps are modules you include

You will often find that some steps need to be used in many features, and in that case it makes sense to put those steps in reusable modules.

The example in the README is a step that logs the user into the site, and it is put in a module with a nested module inside it:

ruby
# ... features/steps/common_steps/login.rb
module CommonSteps
  module Login
    include Spinach::DSL

    step 'I am logged in' do
      # log in stuff...
    end
  end
end

The file lives under features/steps/common_steps/, which keeps shared steps in one place rather than mixed in with the per feature files. Note the include Spinach::DSL, which is what brings the step method into the module.

Using it is then a one line include in any feature class:

ruby
# ... features/steps/buying_a_widget.rb
class Spinach::Features::BuyAWidget < Spinach::FeatureSteps
  # simply include this module and you are good to go
  include CommonSteps::Login
end

This is the reusability design goal made concrete, and it is a mechanism Ruby developers already know. There is no global step registry to collide with, because the step definitions are scoped to the class that includes them. Two features can both define a step called I am logged in with different bodies and nothing breaks, which is precisely the class of problem that makes large Cucumber suites painful to maintain.

The audit command finds drift without touching your files

Over time, the definitions of your features will change, and Spinach gives you a command for keeping the step files in step with them.

When you add, remove or change steps in the feature files, you can audit your existing step files with:

shell
$ spinach --audit

This finds any new steps and prints out boilerplate for them, and alerts you to the filename and line number of any unused steps in your step files.

The important detail is what it does not do. The audit does not modify the step files, so you will need to paste the boilerplate into the appropriate places yourself. And if a new feature file is detected, you are asked to run spinach --generate beforehand. So audit is a report, and generate is the action that writes.

There is a caveat that will bite you the first time you use it on a single file. If auditing individual files, common steps may be reported as unused when they are actually used in a feature file that is not currently being audited. To avoid this, run the audit with no arguments to audit all step files simultaneously. That is a consequence of the class based model, since a shared module has no way to know which feature files reference it.

For a suite with a lot of shared modules, the practical advice is to always audit the whole suite rather than a single file, and to treat the unused step report as a prompt for a human look rather than an automatic deletion.

Tags drive both hooks and the run filter

Feature and scenarios can be marked with tags in the form @tag, and the README lists two purposes for them.

The first is applying actions using hooks, with @javascript, @transaction and @vcr given as examples. The Capybara example shows the shape: Spinach already integrates with Capybara if you add require spinach/capybara in features/support/env.rb, and then a hook switches the driver when a scenario is tagged. The example extracted from that integration registers an on_tag hook for javascript that sets Capybara's current driver to the javascript driver.

The second is filtering. The README gives @module-a, @customer, @admin, @bug-12 and @feat-1 as examples, and the filtering syntax is a small language of its own. You run all scenarios tagged @feat-1 with:

shell
$ spinach --tags @feat-1

You run scenarios tagged either @feat-1 or @bug-12 with a comma:

shell
$ spinach --tags @feat-1,@bug-12

And you run scenarios tagged @feat-1 that are not tagged @bug-12 with a tilde for negation:

shell
$ spinach --tags @feat-1,~@bug-12

There is also a default behaviour worth knowing. By default Spinach will ignore scenarios marked with the tag @wip, or whose feature is marked with @wip. Those are meant to be work in progress, scenarios that are pending while you work on them. To explicitly run those, you use the --tags option with @wip, which overrides the skip.

That default is a small quality of life feature that a lot of BDD runners have and that you will miss if you move away.

A gem with an old release tag and a recent push

The maintenance picture is the one thing to weigh before choosing this framework, and it needs reading carefully.

The only tagged release listed is 0.12.0, from 2023-02-09. The last push to the main branch was on 2026-05-02, and the repository is not archived, so there is work on the branch but no new release tag in more than three years. The gem is on RubyGems as spinach, and the README carries a gem version badge and a tests badge from a GitHub Actions workflow.

The Ruby support matrix is stated explicitly: Spinach is tested against Ruby MRI 2.4, 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 as well as JRuby. That is a wide matrix, but it also tells you the era the project was built in, since MRI 2.4 is long past end of life.

The repository layout is a conventional Ruby gem. There is lib/ for the code, features/ for the framework's own Gherkin specifications, test/ for the unit tests, bin/, a spinach.gemspec, a Rakefile, a Gemfile, a Guardfile, a CHANGELOG.md and a README.markdown. There is a .ruby-version, a .ruby-gemset and a .tool-versions, so the intended Ruby version is pinned in the repository in three different ways, which is what a team that works across version managers ends up with.

The practical question is whether the class based step model is worth the ecosystem cost. If your team is a Ruby shop that has already decided it wants Gherkin specifications and plain Ruby step logic, Spinach is a small, readable codebase that does exactly that. If you are choosing a BDD framework from scratch, the fact that the only release is from 2023 is the first thing you will be asked about.

Editorial conclusion

Adopt Spinach if your team already writes Ruby, wants acceptance tests in Gherkin, and values keeping step logic in ordinary classes and modules over Cucumber's world step registry. Do not adopt it if you need a framework with a current ecosystem, a large plugin set, or active maintenance, because the only tagged release is 0.12.0 from 2023-02-09 and the last push was on 2026-05-02. Before you start, check which Ruby you run, since the README states the test matrix covers MRI 2.4 through 3.2 as well as JRuby, and run spinach --audit after you change a feature file, because the audit command reports unused steps without rewriting your files.

Frequently asked questions

How do I install and set up Spinach?

Add spinach to your Gemfile in the test group, create a features folder, write a feature file, then run `spinach --generate` to scaffold the step class. Running `spinach` again executes the features. The generator derives the class name from the feature name in camelcase and writes it under features/steps.

What is different about Spinach compared to Cucumber?

In Spinach each feature maps to its own class and each step is a method on that class, which the project lists as its step maintainability design goal, and shared steps are plain Ruby modules you include. Cucumber instead matches step text against a global registry of definitions.

How do I share steps between features in Spinach?

Put the steps in a module under features/steps/common_steps/, include Spinach::DSL in it, and then include that module in any feature class that needs it. Because the step definitions are scoped to the including class, two features can use the same step text with different bodies without a collision.

How do I filter Spinach scenarios by tag?

Use `spinach --tags @feat-1` for one tag, `spinach --tags @feat-1,@bug-12` for either, and `spinach --tags @feat-1,~@bug-12` to exclude. By default Spinach skips scenarios or features tagged @wip, and `spinach --tags @wip` runs those explicitly.

Official sources

  1. codegram/spinach on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/codegram-spinach.svg)](https://hysenlabs.com/projects/codegram-spinach)

Was this useful?