├── .document ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── .tool-versions ├── CHANGELOG.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.markdown ├── Rakefile ├── bin └── spinach ├── features ├── automatic_feature_generation.feature ├── background.feature ├── before_and_after_hooks.feature ├── before_and_after_hooks_inheritance.feature ├── exit_status.feature ├── fail_fast.feature ├── feature_hooks_and_tags.feature ├── feature_name_guessing.feature ├── pending_steps.feature ├── randomization.feature ├── reporting │ ├── customized_reporter.feature │ ├── display_run_summary.feature │ ├── error_reporting.feature │ ├── pending_feature_reporting.feature │ ├── show_step_source_location.feature │ └── undefined_feature_reporting.feature ├── rspec_compatibility.feature ├── running_specific_scenarios.feature ├── step_auditing.feature ├── steps │ ├── automatic_feature_generation.rb │ ├── background.rb │ ├── before_and_after_hooks.rb │ ├── before_and_after_hooks_inheritance.rb │ ├── exit_status.rb │ ├── fail_fast_option.rb │ ├── feature_hooks_and_tags.rb │ ├── feature_name_guessing.rb │ ├── pending_steps.rb │ ├── randomizing_features_scenarios.rb │ ├── reporting │ │ ├── display_run_summary.rb │ │ ├── error_reporting.rb │ │ ├── pending_feature_reporting.rb │ │ ├── show_step_source_location.rb │ │ ├── undefined_feature_reporting.rb │ │ └── use_customized_reporter.rb │ ├── rspec_compatibility.rb │ ├── running_specific_scenarios.rb │ └── step_auditing.rb └── support │ ├── env.rb │ ├── error_reporting.rb │ ├── feature_generator.rb │ ├── filesystem.rb │ └── spinach_runner.rb ├── lib ├── spinach.rb └── spinach │ ├── auditor.rb │ ├── background.rb │ ├── capybara.rb │ ├── cli.rb │ ├── config.rb │ ├── dsl.rb │ ├── exceptions.rb │ ├── feature.rb │ ├── feature_steps.rb │ ├── features.rb │ ├── frameworks.rb │ ├── frameworks │ ├── minitest.rb │ └── rspec.rb │ ├── generators.rb │ ├── generators │ ├── feature_generator.rb │ └── step_generator.rb │ ├── hookable.rb │ ├── hooks.rb │ ├── orderers.rb │ ├── orderers │ ├── default.rb │ └── random.rb │ ├── parser.rb │ ├── parser │ └── visitor.rb │ ├── reporter.rb │ ├── reporter │ ├── failure_file.rb │ ├── progress.rb │ ├── reporting.rb │ └── stdout.rb │ ├── rspec │ └── mocks.rb │ ├── runner.rb │ ├── runner │ ├── feature_runner.rb │ └── scenario_runner.rb │ ├── scenario.rb │ ├── step.rb │ ├── support.rb │ ├── tags_matcher.rb │ └── version.rb ├── spinach.gemspec └── test ├── spinach ├── background_test.rb ├── capybara_test.rb ├── cli_test.rb ├── config_test.rb ├── dsl_test.rb ├── feature_steps_test.rb ├── feature_test.rb ├── frameworks │ └── minitest_test.rb ├── generators │ ├── feature_generator_test.rb │ └── step_generator_test.rb ├── generators_test.rb ├── hookable_test.rb ├── hooks_test.rb ├── orderers │ ├── default_test.rb │ └── random_test.rb ├── parser │ └── visitor_test.rb ├── parser_test.rb ├── reporter │ ├── failure_file_test.rb │ ├── progress_test.rb │ ├── stdout │ │ └── error_reporting_test.rb │ └── stdout_test.rb ├── reporter_test.rb ├── runner │ ├── feature_runner_test.rb │ └── scenario_runner_test.rb ├── runner_test.rb ├── scenario_test.rb ├── step_test.rb ├── support_test.rb └── tags_matcher_test.rb ├── spinach_test.rb ├── support └── filesystem.rb └── test_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.markdown 2 | lib/**/*.rb 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | env: 12 | CI: "true" 13 | 14 | jobs: 15 | main: 16 | name: Tests 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | ruby: [2.4, 2.5, 2.6, 2.7, "3.0", 3.1, 3.2, jruby] 21 | fail-fast: false 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 1 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | bundler-cache: true 30 | - run: bundle exec rake 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .rbx 4 | .bundle 5 | .config 6 | .yardoc 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | capybara-*.html 19 | Gemfile.lock 20 | tags 21 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | spinach -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.2.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | == 0.8.6 2 | * add total run time after each run 3 | * fixed #145: Issue with step autoloading 4 | 5 | == 0.8.4 6 | * fixed #138: The pending steps should abort the remaining scenario, but continue running other scenarios 7 | * added the feature #140: Allow blockless step definitions 8 | 9 | == 0.8.3 10 | * add ```--fail-fast``` option. when specified, the suite will terminate after the first failed scenario 11 | 12 | == 0.8.2 13 | * upgrade to gherkin-ruby 0.3 in order to avoid naming conflicts when using at 14 | the same time spinach & cucumber (transitioning) 15 | 16 | == 0.8.1 17 | * bug fix 18 | * Requiring `spinach/capybara` now auto-includes Capybara's DSL 19 | 20 | == 0.8.0 21 | 22 | * backwards incompatible changes 23 | * Pending steps no longer exit with -1 24 | 25 | * bug fix 26 | * Nothing 27 | 28 | * enhancements 29 | * Add CHANGELOG 30 | * Add progress reporter 31 | * Add official ruby 2.0.0 support 32 | 33 | * deprecations 34 | * Nothing 35 | 36 | == 0.7.0 37 | 38 | * backwards incompatible changes 39 | * Nothing 40 | 41 | * bug fix 42 | * Nothing 43 | 44 | * enhancements 45 | * Steps are now generated with the `step` keyword instead of `Given`, `When`, `Then`, etc.. 46 | * Generated features are namespaced to the `Spinach::Features` to prevent conflicts. 47 | 48 | * deprecations 49 | * Nothing 50 | 51 | == 0.6.1 52 | 53 | * backwards incompatible changes 54 | * Nothing 55 | 56 | * bug fix 57 | * Don't run entire test suite when an inexisting feature file is given. 58 | * Run all features from given args instead of just the first one. 59 | * Don't run tests when the `--generate` flag is given. 60 | 61 | * enhancements 62 | * Add docs on how to use shared steps. 63 | 64 | * deprecations 65 | * Nothing 66 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | # Specify your gem's dependencies in spinach.gemspec 4 | gemspec 5 | 6 | gem 'pry-byebug', platforms: [:ruby] 7 | 8 | group :docs do 9 | gem 'yard' 10 | end 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'minitest' do 2 | watch(%r|^test/(.*)_test\.rb|) 3 | watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}#{m[2]}_test.rb" } 4 | watch(%r|^test/test_helper\.rb|) { "test" } 5 | end 6 | 7 | guard 'spinach' do 8 | watch(%r|^features/(.*)\.feature|) 9 | watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m| 10 | "features/#{m[1]}#{m[2]}.feature" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) <2013> 2 | 3 | MIT License (Expat) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rake/testtask' 6 | Rake::TestTask.new do |t| 7 | t.libs << "test" 8 | t.test_files = FileList['./test/**/*_test.rb'] 9 | # t.loader = :direct 10 | end 11 | 12 | desc 'Run spinach features' 13 | task :spinach do 14 | exec "bin/spinach --rand" 15 | end 16 | 17 | task :default => [:test, :spinach] 18 | -------------------------------------------------------------------------------- /bin/spinach: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | begin 4 | require "bundler/setup" 5 | rescue LoadError 6 | end 7 | 8 | begin 9 | require 'spinach' 10 | rescue LoadError 11 | require_relative '../lib/spinach' 12 | end 13 | 14 | cli = Spinach::Cli.new(ARGV) 15 | exit cli.run 16 | -------------------------------------------------------------------------------- /features/automatic_feature_generation.feature: -------------------------------------------------------------------------------- 1 | Feature: Automatic feature generation 2 | In order to be faster writing features 3 | As a developer 4 | I want spinach to automatically generate features for me 5 | 6 | Scenario: Missing feature 7 | Given I have defined a "Cheezburger can I has" feature 8 | When I run spinach with "--generate" 9 | Then I a feature should exist named "features/steps/cheezburger_can_i_has.rb" 10 | And that feature should have the example feature steps 11 | -------------------------------------------------------------------------------- /features/background.feature: -------------------------------------------------------------------------------- 1 | Feature: Background 2 | In order to avoid duplication of Steps in Scenarios 3 | As a developer 4 | I want to describe the background Steps once 5 | 6 | Background: 7 | Given Spinach has Background support 8 | 9 | Scenario: Using Background 10 | And another step in this scenario 11 | When I run this Scenario 12 | Then the background step should have been executed 13 | And the scenario step should have been executed 14 | -------------------------------------------------------------------------------- /features/before_and_after_hooks.feature: -------------------------------------------------------------------------------- 1 | Feature: Before and after hooks 2 | In order to setup and clean the test environment for a single feature 3 | As a developer 4 | I want to be able to use before and after hooks within the step class 5 | 6 | Scenario: Happy path 7 | Then I can verify the variable setup in the before hook 8 | 9 | Scenario: Inter-dependency - after hook cleans up (after happy path) 10 | Then I can verify the variable setup in the before hook 11 | -------------------------------------------------------------------------------- /features/before_and_after_hooks_inheritance.feature: -------------------------------------------------------------------------------- 1 | Feature: Before and after hooks inheritance 2 | In order to maintain the super classes' before and after code 3 | As a developer 4 | I want to chain the before and after blocks 5 | 6 | Scenario: Happy path 7 | Then I can see the variable setup in the super class before hook 8 | And I can see the variable being overridden in the subclass 9 | 10 | Scenario: Inter-dependency - after hook cleans up (after happy path) 11 | Then I can see the variable setup in the super class before hook 12 | And I can see the variable being overridden in the subclass 13 | -------------------------------------------------------------------------------- /features/exit_status.feature: -------------------------------------------------------------------------------- 1 | Feature: Exit status 2 | In order to receive a standard exit code 3 | As a developer 4 | I want spinach to return exit status properly 5 | 6 | Scenario: It succeeds 7 | Given I have a feature that has no error or failure 8 | When I run it 9 | Then the exit status should be 0 10 | 11 | Scenario: It fails 12 | Given I have a feature that has a failure 13 | When I run it 14 | Then the exit status should be 1 15 | -------------------------------------------------------------------------------- /features/fail_fast.feature: -------------------------------------------------------------------------------- 1 | Feature: Fail fast option 2 | In order to save running time 3 | As a developer 4 | I want spinach to fail fast when I so desire 5 | 6 | Scenario: fail fast 7 | Given I have a feature that has a failure 8 | And I have a feature that has no error or failure 9 | When I run both of them with fail-fast option 10 | Then the tests stop at the first one 11 | -------------------------------------------------------------------------------- /features/feature_hooks_and_tags.feature: -------------------------------------------------------------------------------- 1 | Feature: Feature Hooks and Tags 2 | In order to run only the appropriate setup and teardown code 3 | As a developer 4 | I want spinach to only run feature hooks if those features would be run under the tags I provided 5 | 6 | Scenario: No tags specified 7 | Given I have a tagged feature with an untagged scenario 8 | And I have an untagged feature with a tagged scenario 9 | When I don't specify tags 10 | Then all the feature hooks should have run 11 | 12 | Scenario: Tags specified 13 | Given I have a tagged feature with an untagged scenario 14 | And I have an untagged feature with a tagged scenario 15 | When I specify a tag the features and scenarios are tagged with 16 | Then all the feature hooks should have run 17 | 18 | Scenario: Tags excluded 19 | Given I have a tagged feature with an untagged scenario 20 | And I have an untagged feature with a tagged scenario 21 | When I exclude a tag the features and scenarios are tagged with 22 | Then no feature hooks should have run 23 | -------------------------------------------------------------------------------- /features/feature_name_guessing.feature: -------------------------------------------------------------------------------- 1 | Feature: Feature name guessing 2 | In order to be faster writing steps 3 | As a test writer 4 | I want the names of the features to be guessed from the feature class name 5 | 6 | Scenario: Basic guess 7 | Given I am writing a feature called "My cool feature" 8 | And I write a class named "MyCoolFeature" 9 | When I run spinach 10 | Then I want "MyCoolFeature" class to be used to run it 11 | -------------------------------------------------------------------------------- /features/pending_steps.feature: -------------------------------------------------------------------------------- 1 | Feature: Pending steps 2 | In order to save running time 3 | As a developer 4 | I want spinach to fail fast when I so desire 5 | 6 | Scenario: multiple scenarios 7 | Given I have a feature that has a pending step 8 | When I run the feature 9 | Then the test stops at the pending step and reported as such 10 | -------------------------------------------------------------------------------- /features/randomization.feature: -------------------------------------------------------------------------------- 1 | Feature: Randomizing Features & Scenarios 2 | In order to ensure my tests aren't dependent 3 | As a developer 4 | I want spinach to randomize features and scenarios (but not steps) 5 | 6 | Scenario: Randomizing the run without specifying a seed 7 | Given I have 2 features with 2 scenarios each 8 | When I randomize the run without specifying a seed 9 | Then The features and scenarios are run 10 | And The runner output shows a seed 11 | 12 | Scenario: Specifying the seed 13 | Given I have 2 features with 2 scenarios each 14 | When I specify the seed for the run 15 | Then The features and scenarios are run in a different order 16 | And The runner output shows the seed 17 | -------------------------------------------------------------------------------- /features/reporting/customized_reporter.feature: -------------------------------------------------------------------------------- 1 | Feature: Use customized reporter 2 | As a developer 3 | I want to use a different reporter 4 | So I can have different output 5 | 6 | Scenario: Happy path 7 | Given I have a feature that has no error or failure 8 | When I run it using the new reporter 9 | Then I see the desired output 10 | 11 | Scenario: Multiple reporters 12 | Given I have a feature that has one failure 13 | When I run it using two custom reporters 14 | Then I see one reporter's output on the screen 15 | And I see the other reporter's output in a file -------------------------------------------------------------------------------- /features/reporting/display_run_summary.feature: -------------------------------------------------------------------------------- 1 | Feature: Display run summary 2 | As a developer 3 | I want spinach to display a summary of steps statuses 4 | So I can easily know general features status 5 | 6 | Scenario: Display run summary at the end of features run without randomization 7 | Given I have a feature that has some successful, undefined, failed and error steps 8 | 9 | When I run it without randomization 10 | Then I should see a summary with steps status information 11 | And I shouldn't see a randomization seed 12 | 13 | Scenario: Display run summary at the end of features run with randomization 14 | Given I have a feature that has some successful, undefined, failed and error steps 15 | 16 | When I run it with randomization 17 | Then I should see a summary with steps status information 18 | And I should see a randomization seed 19 | 20 | Scenario: Display run summary at the end of features run with a randomization seed 21 | Given I have a feature that has some successful, undefined, failed and error steps 22 | 23 | When I run it with a specific randomization seed 24 | Then I should see a summary with steps status information 25 | And I should see that specific randomization seed 26 | -------------------------------------------------------------------------------- /features/reporting/error_reporting.feature: -------------------------------------------------------------------------------- 1 | Feature: Error reporting 2 | In order to receive a clear output and chase my errors 3 | As a developer 4 | I want spinach to give me a comprehensive error reporting 5 | 6 | Scenario: Error reporting without backtrace 7 | Given I have a feature with some failures 8 | When I run "spinach" 9 | Then I should see the failure count along with their messages 10 | 11 | Scenario: Error reporting with backtrace 12 | Given I have a feature with some failures 13 | When I run "spinach --backtrace" 14 | Then I should see the error count along with their messages and backtrace 15 | -------------------------------------------------------------------------------- /features/reporting/pending_feature_reporting.feature: -------------------------------------------------------------------------------- 1 | Feature: Pending feature reporting 2 | In order to be aware of what features are still pending 3 | As a developer 4 | I want spinach to tell me which of them I still need to implement 5 | 6 | Scenario: Pending feature 7 | Given I've written a pending scenario 8 | When I run spinach 9 | Then I should see a message telling me that there's a pending scenario 10 | And I should see a message showing me the reason of the pending scenario 11 | 12 | Scenario: Step definition without body 13 | Given I've written a step definition without body 14 | When I run spinach 15 | Then I should see a message telling me that there's a pending scenario 16 | And I should see a message showing me the default reason of the pending scenario 17 | -------------------------------------------------------------------------------- /features/reporting/show_step_source_location.feature: -------------------------------------------------------------------------------- 1 | Feature: Show step source location 2 | As a developer 3 | I want spinach to give me every step source location in output 4 | So I can easily know where I defined a step 5 | 6 | Scenario: Show class steps source location in output when all is ok 7 | Given I have a feature that has no error or failure 8 | When I run it 9 | Then I should see the source location of each step of every scenario 10 | 11 | Scenario: Show in output the source location of external modules steps 12 | Given I have a feature that has no error or failure and use external steps 13 | When I run it 14 | Then I should see the source location of each step, even external ones 15 | 16 | Scenario: Show class steps source location in output even when there is an error 17 | Given I have a feature that has an error 18 | When I run it 19 | Then I should see the source location of each step, even ones with errors 20 | 21 | Scenario: Show class steps source location in output even when there is a failure 22 | Given I have a feature that has a failure 23 | When I run it 24 | Then I should see the source location of each step, even ones with failures 25 | -------------------------------------------------------------------------------- /features/reporting/undefined_feature_reporting.feature: -------------------------------------------------------------------------------- 1 | Feature: Undefined feature reporting 2 | In order to be aware of what features I've still not defined 3 | As a developer 4 | I want spinach to tell me which of them I'm missing 5 | 6 | Scenario: Undefined feature 7 | Given I've written a feature but not its steps 8 | When I run spinach 9 | Then I should see a message telling me that there's an undefined feature 10 | -------------------------------------------------------------------------------- /features/rspec_compatibility.feature: -------------------------------------------------------------------------------- 1 | Feature: RSpec compatibility 2 | In order to use rspec in my step definitions 3 | As a RSpec developer 4 | I want spinach to detect my rspec failures as failures instead of errors 5 | 6 | Scenario: Everything works as expected 7 | Given I have a feature that should completely pass 8 | When I run "spinach" with rspec 9 | Then there should be no error 10 | 11 | Scenario: An expectation fails 12 | Given I have a feature with some failed expectations 13 | When I run "spinach" with rspec 14 | Then I should see the failure count along with their messages 15 | 16 | Scenario: RSpec with capybara 17 | Given I have a sinatra app with some capybara-based expectations 18 | When I run "spinach" with rspec 19 | Then there should be no error 20 | -------------------------------------------------------------------------------- /features/running_specific_scenarios.feature: -------------------------------------------------------------------------------- 1 | Feature: Running Specific Scenarios 2 | In order to test only specific scenarios 3 | As a developer 4 | I want spinach to run only the scenarios I specify 5 | 6 | Scenario: Specifying line numbers 7 | Given I have a feature with 2 scenarios 8 | When I specify that only the second should be run 9 | Then One will succeed and none will fail 10 | 11 | Scenario: Including tags 12 | Given I have a tagged feature with 2 scenarios 13 | When I include the tag of the failing scenario 14 | Then None will succeed and one will fail 15 | 16 | Scenario: Excluding tags 17 | Given I have a tagged feature with 2 scenarios 18 | When I exclude the tag of the passing scenario 19 | Then None will succeed and one will fail 20 | 21 | Scenario: Combining tags 22 | Given I have a tagged feature with 2 scenarios 23 | When I include the tag of the feature and exclude the tag of the failing scenario 24 | Then One will succeed and none will fail 25 | -------------------------------------------------------------------------------- /features/step_auditing.feature: -------------------------------------------------------------------------------- 1 | Feature: Step auditing 2 | In order to be able to update my features 3 | As a developer 4 | I want spinach to automatically audit which steps are missing and obsolete 5 | 6 | Scenario: Step file out of date 7 | Given I have defined a "Cheezburger can I has" feature 8 | And I have an associated step file with missing steps and obsolete steps 9 | When I run spinach with "--audit" 10 | Then I should see a list of unused steps 11 | And I should see the code to paste for missing steps 12 | 13 | Scenario: With common steps 14 | Given I have defined a "Cheezburger can I has" feature 15 | And I have an associated step file with some steps in a common module 16 | When I run spinach with "--audit" 17 | Then I should not see any steps marked as missing 18 | 19 | Scenario: Steps not marked unused if they're in common modules 20 | Given I have defined a "Cheezburger can I has" feature 21 | And I have defined an "Awesome new feature" feature 22 | And I have associated step files with common steps that are all used somewhere 23 | When I run spinach with "--audit" 24 | Then I should not see any steps marked as unused 25 | 26 | Scenario: Common steps are reported as missing if not used by any feature 27 | Given I have defined a "Cheezburger can I has" feature 28 | And I have defined an "Awesome new feature" feature 29 | And I have step files for both with common steps, but one common step is not used by either 30 | When I run spinach with "--audit" 31 | Then I should be told the extra step is unused 32 | But I should not be told the other common steps are unused 33 | 34 | Scenario: Tells the user to generate if step file missing 35 | Given I have defined a "Cheezburger can I has" feature 36 | And I have not created an associated step file 37 | When I run spinach with "--audit" 38 | Then I should be told to run "--generate" 39 | 40 | Scenario: Steps still marked unused if they appear in the wrong file 41 | Given I have defined a "Cheezburger can I has" feature 42 | And I have defined an "Awesome new feature" feature 43 | And I have created a step file for each with a step from one feature pasted into the other's file 44 | When I run spinach with "--audit" 45 | Then I should be told that step is unused 46 | 47 | Scenario: Reports a clean audit if no steps are missing 48 | Given I have defined a "Cheezburger can I has" feature 49 | And I have defined an "Awesome new feature" feature 50 | And I have complete step files for both 51 | When I run spinach with "--audit" 52 | Then I should be told this was a clean audit 53 | 54 | Scenario: Should not report a step as missing more than once 55 | Given I have defined an "Exciting feature" feature with reused steps 56 | And I have created a step file without those reused steps 57 | When I run spinach with "--audit" 58 | Then I should see the missing steps reported only once 59 | 60 | -------------------------------------------------------------------------------- /features/steps/automatic_feature_generation.rb: -------------------------------------------------------------------------------- 1 | class AutomaticFeatureGeneration < Spinach::FeatureSteps 2 | 3 | feature 'Automatic feature generation' 4 | 5 | include Integration::SpinachRunner 6 | Given 'I have defined a "Cheezburger can I has" feature' do 7 | write_file('features/cheezburger_can_i_has.feature', """ 8 | Feature: Cheezburger can I has 9 | Scenario: Some Lulz 10 | Given I haz a sad 11 | When I get some lulz 12 | Then I haz a happy 13 | """) 14 | end 15 | 16 | When 'I run spinach with "--generate"' do 17 | run_feature 'features/cheezburger_can_i_has.feature', append: '--generate' 18 | end 19 | 20 | Then 'I a feature should exist named "features/steps/cheezburger_can_i_has.rb"' do 21 | in_current_dir do 22 | @file = 'features/steps/cheezburger_can_i_has.rb' 23 | File.exist?(@file).must_equal true 24 | end 25 | end 26 | 27 | And "that feature should have the example feature steps" do 28 | in_current_dir do 29 | content = File.read(@file) 30 | content.must_include "I haz a sad" 31 | content.must_include "I get some lulz" 32 | content.must_include "I haz a happy" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /features/steps/background.rb: -------------------------------------------------------------------------------- 1 | class Background < Spinach::FeatureSteps 2 | 3 | feature 'Background' 4 | 5 | def initialize 6 | @background_step = false 7 | @scenario_step = false 8 | end 9 | 10 | And 'another step in this scenario' do 11 | @scenario_step = true 12 | end 13 | 14 | When 'I run this Scenario' do 15 | # Nothing to be done. 16 | end 17 | 18 | Then 'the background step should have been executed' do 19 | @background_step.must_equal true 20 | end 21 | 22 | Then 'the scenario step should have been executed' do 23 | @scenario_step.must_equal true 24 | end 25 | 26 | Given 'Spinach has Background support' do 27 | @background_step = true 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /features/steps/before_and_after_hooks.rb: -------------------------------------------------------------------------------- 1 | class BeforeAndAfterHooks < Spinach::FeatureSteps 2 | class << self 3 | attr_accessor :var1 4 | end 5 | 6 | before do 7 | if self.class.var1.nil? 8 | self.class.var1 = :clean 9 | else 10 | self.class.var1 = :dirty 11 | end 12 | end 13 | 14 | after do 15 | self.class.var1 = nil 16 | end 17 | 18 | Then 'I can verify the variable setup in the before hook' do 19 | self.class.var1.must_equal :clean 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /features/steps/before_and_after_hooks_inheritance.rb: -------------------------------------------------------------------------------- 1 | class BeforeAndAfterHooksInheritanceBase < Spinach::FeatureSteps 2 | class << self 3 | attr_accessor :var1 4 | attr_accessor :var2 5 | end 6 | 7 | before do 8 | if self.class.var2.nil? 9 | self.class.var2 = :i_am_here 10 | else 11 | self.class.var2 = :dirty 12 | end 13 | if self.class.var1.nil? 14 | self.class.var1 = :clean 15 | else 16 | self.class.var1 = :dirty 17 | end 18 | end 19 | 20 | after do 21 | self.class.var1 = nil 22 | end 23 | 24 | end 25 | 26 | class BeforeAndAfterHooksInheritance < BeforeAndAfterHooksInheritanceBase 27 | before do 28 | self.class.var1 = :in_subclass 29 | end 30 | 31 | after do 32 | self.class.var2 = nil 33 | end 34 | 35 | Then 'I can see the variable being overridden in the subclass' do 36 | self.class.var1.must_equal :in_subclass 37 | end 38 | 39 | Then "I can see the variable setup in the super class before hook" do 40 | self.class.var2.must_equal :i_am_here 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /features/steps/exit_status.rb: -------------------------------------------------------------------------------- 1 | class ExitStatus < Spinach::FeatureSteps 2 | 3 | feature "Exit status" 4 | 5 | include Integration::SpinachRunner 6 | 7 | Given "I have a feature that has no error or failure" do 8 | @feature = Integration::FeatureGenerator.success_feature 9 | end 10 | 11 | Given "I have a feature that has a failure" do 12 | @feature = Integration::FeatureGenerator.failure_feature 13 | end 14 | 15 | When "I run it" do 16 | run_feature @feature 17 | end 18 | 19 | Then "the exit status should be 0" do 20 | @last_exit_status.success?.must_equal true 21 | end 22 | 23 | Then "the exit status should be 1" do 24 | @last_exit_status.success?.must_equal false 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /features/steps/fail_fast_option.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::FailFastOption < Spinach::FeatureSteps 2 | include Integration::SpinachRunner 3 | 4 | step 'I have a feature that has a failure' do 5 | @features ||= [] 6 | @features << Integration::FeatureGenerator.failure_feature_with_two_scenarios 7 | end 8 | 9 | step 'I have a feature that has no error or failure' do 10 | @features ||= [] 11 | @features << Integration::FeatureGenerator.success_feature 12 | end 13 | 14 | step 'I run both of them with fail-fast option' do 15 | run_feature @features.join(" "), append: '--fail-fast' 16 | end 17 | 18 | step 'the tests stop at the first one' do 19 | @stdout.must_match("(0) Successful") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /features/steps/feature_hooks_and_tags.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::FeatureHooksAndTags < Spinach::FeatureSteps 2 | include Integration::SpinachRunner 3 | 4 | step 'I have a tagged feature with an untagged scenario' do 5 | write_file 'features/a.feature', <<-FEATURE 6 | @tag 7 | Feature: A 8 | Scenario: A1 9 | Then a1 10 | FEATURE 11 | 12 | write_file 'features/steps/a.rb', <<-STEPS 13 | class Spinach::Features::A < Spinach::FeatureSteps 14 | step 'a1' do; end 15 | end 16 | STEPS 17 | end 18 | 19 | step 'I have an untagged feature with a tagged scenario' do 20 | write_file 'features/b.feature', <<-FEATURE 21 | Feature: B 22 | @tag 23 | Scenario: B1 24 | Then b1 25 | FEATURE 26 | 27 | write_file 'features/steps/b.rb', <<-STEPS 28 | class Spinach::Features::B < Spinach::FeatureSteps 29 | step 'b1' do; end 30 | end 31 | STEPS 32 | end 33 | 34 | step "I don't specify tags" do 35 | run_spinach 36 | end 37 | 38 | step 'I specify a tag the features and scenarios are tagged with' do 39 | run_spinach({append: "--tags @tag"}) 40 | end 41 | 42 | step 'I exclude a tag the features and scenarios are tagged with' do 43 | run_spinach({append: "--tags ~@tag"}) 44 | end 45 | 46 | step 'all the feature hooks should have run' do 47 | @stdout.must_match("Feature: A") 48 | @stdout.must_match("Feature: B") 49 | end 50 | 51 | step 'no feature hooks should have run' do 52 | @stdout.wont_match("Feature: A") 53 | @stdout.wont_match("Feature: B") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /features/steps/feature_name_guessing.rb: -------------------------------------------------------------------------------- 1 | class FeatureNameGuessing < Spinach::FeatureSteps 2 | 3 | feature "Feature name guessing" 4 | 5 | include Integration::SpinachRunner 6 | 7 | Given 'I am writing a feature called "My cool feature"' do 8 | write_file('features/my_cool_feature.feature', """ 9 | Feature: My cool feature 10 | 11 | Scenario: This is scenario is cool 12 | When this is so meta 13 | Then the world is crazy 14 | """) 15 | end 16 | 17 | And 'I write a class named "MyCoolFeature"' do 18 | write_file('features/steps/my_cool_feature.rb', 19 | 'class MyCoolFeature < Spinach::FeatureSteps 20 | When "this is so meta" do 21 | end 22 | 23 | Then "the world is crazy" do 24 | end 25 | end') 26 | end 27 | 28 | When 'I run spinach'do 29 | run_feature 'features/my_cool_feature.feature' 30 | end 31 | 32 | Then 'I want "MyCoolFeature" class to be used to run it' do 33 | @last_exit_status.must_equal 0 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /features/steps/pending_steps.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::PendingSteps < Spinach::FeatureSteps 2 | include Integration::SpinachRunner 3 | 4 | step 'I have a feature that has a pending step' do 5 | @feature = Integration::FeatureGenerator.pending_feature_with_multiple_scenario 6 | end 7 | 8 | step 'I run the feature' do 9 | run_feature @feature 10 | end 11 | 12 | step 'the test stops at the pending step and reported as such' do 13 | @stdout.must_match("(1) Successful") 14 | @stdout.must_match("(0) Failed") 15 | @stdout.must_match("(1) Pending") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /features/steps/randomizing_features_scenarios.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::RandomizingFeaturesScenarios < Spinach::FeatureSteps 2 | include Integration::SpinachRunner 3 | 4 | step 'I have 2 features with 2 scenarios each' do 5 | write_file 'features/success_a.feature', <<-FEATURE 6 | Feature: Success A 7 | Scenario: A1 8 | Then a1 9 | Scenario: A2 10 | Then a2 11 | FEATURE 12 | 13 | write_file 'features/steps/success_a.rb', <<-STEPS 14 | class Spinach::Features::SuccessA < Spinach::FeatureSteps 15 | step 'a1' do; end 16 | step 'a2' do; end 17 | end 18 | STEPS 19 | 20 | write_file 'features/success_b.feature', <<-FEATURE 21 | Feature: Success B 22 | Scenario: B1 23 | Then b1 24 | Scenario: B2 25 | Then b2 26 | FEATURE 27 | 28 | write_file 'features/steps/success_b.rb', <<-STEPS 29 | class Spinach::Features::SuccessB < Spinach::FeatureSteps 30 | step 'b1' do; end 31 | step 'b2' do; end 32 | end 33 | STEPS 34 | end 35 | 36 | step 'I randomize the run without specifying a seed' do 37 | run_spinach({append: "--rand"}) 38 | end 39 | 40 | step 'I specify the seed for the run' do 41 | # Reverse order (A2 A1 B2 B1) is the only way I can show that 42 | # scenarios and features are randomized by the seed when the 43 | # example has 2 features each with 2 scenarios. I tried seeds 44 | # until I found one that ordered the test in that order. 45 | @seed = 1 46 | 47 | run_spinach({append: "--seed #{@seed}"}) 48 | end 49 | 50 | step 'The features and scenarios are run' do 51 | @stdout.must_include("A1") 52 | @stdout.must_include("A2") 53 | @stdout.must_include("B1") 54 | @stdout.must_include("B2") 55 | end 56 | 57 | step 'The features and scenarios are run in a different order' do 58 | @stdout.must_match(/B2.*B1.*A2.*A1/m) 59 | end 60 | 61 | step 'The runner output shows a seed' do 62 | @stdout.must_match(/^Randomized with seed \d*$/) 63 | end 64 | 65 | step 'The runner output shows the seed' do 66 | @stdout.must_match(/^Randomized with seed #{@seed}$/) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /features/steps/reporting/display_run_summary.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class DisplayRunSummary < Spinach::FeatureSteps 4 | 5 | feature 'automatic' 6 | 7 | include Integration::SpinachRunner 8 | 9 | Given "I have a feature that has some successful, undefined, failed and error steps" do 10 | write_file('features/test_feature.feature', """ 11 | 12 | Feature: A test feature 13 | 14 | Scenario: Undefined scenario 15 | Given I am a fool 16 | When I jump from Codegrams roof 17 | Then I must be pwned by floor 18 | 19 | Scenario: Failed scenario 20 | Given I love risk 21 | When I jump from Codegrams roof 22 | Then my parachute must open 23 | Then I must not be pwned by floor 24 | 25 | Scenario: Error scenario 26 | Given I am not a fool 27 | When I go downstairs 28 | Then I must succeed 29 | """) 30 | 31 | write_file('features/steps/test_feature.rb', 32 | 'class ATestFeature < Spinach::FeatureSteps 33 | feature "A test feature" 34 | 35 | Given "I am a fool" do 36 | end 37 | 38 | When "I jump from Codegrams roof" do 39 | end 40 | 41 | Given "I love risk" do 42 | end 43 | 44 | And "my parachute must open" do 45 | false.must_equal true 46 | end 47 | 48 | Given "I am a fool" do 49 | end 50 | 51 | Given "I am not a fool" do 52 | adaksjdald 53 | end 54 | 55 | When "I go downstairs" do 56 | end 57 | 58 | Then "I must succeed" do 59 | true 60 | end 61 | end') 62 | @feature = "features/test_feature.feature" 63 | end 64 | 65 | When "I run it without randomization" do 66 | run_feature @feature 67 | end 68 | 69 | Then "I should see a summary with steps status information" do 70 | @stdout.must_match( 71 | /Summary:.*4.*Successful.*1.*Undefined.*1.*Failed.*1.*Error/ 72 | ) 73 | end 74 | 75 | And "I shouldn't see a randomization seed" do 76 | @stdout.wont_match( 77 | /Randomized\ with\ seed\ \d+/ 78 | ) 79 | end 80 | 81 | When "I run it with randomization" do 82 | run_feature @feature, {append: "--rand"} 83 | end 84 | 85 | And "I should see a randomization seed" do 86 | @stdout.must_match( 87 | /Randomized\ with\ seed\ \d+/ 88 | ) 89 | end 90 | 91 | When "I run it with a specific randomization seed" do 92 | @seed = rand(0xFFFF) 93 | 94 | run_feature @feature, {append: "--seed #{@seed}"} 95 | end 96 | 97 | And "I should see that specific randomization seed" do 98 | @stdout.must_match( 99 | /Randomized\ with\ seed\ #{@seed}/ 100 | ) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /features/steps/reporting/error_reporting.rb: -------------------------------------------------------------------------------- 1 | class ErrorReporting < Spinach::FeatureSteps 2 | 3 | feature "Error reporting" 4 | 5 | include Integration::SpinachRunner 6 | include Integration::ErrorReporting 7 | 8 | Given "I have a feature with some failures" do 9 | write_file('features/feature_with_failures.feature', """ 10 | Feature: Feature with failures 11 | 12 | Scenario: This scenario will fail 13 | Given true is false 14 | Then remove all the files in my hard drive 15 | """) 16 | 17 | write_file('features/steps/failure_feature.rb', 18 | 'class FeatureWithFailures < Spinach::FeatureSteps 19 | feature "Feature with failures" 20 | 21 | Given "true is false" do 22 | true.must_equal false 23 | end 24 | 25 | Then "remove all the files in my hard drive" do 26 | # joking! 27 | end 28 | end') 29 | end 30 | 31 | When 'I run "spinach"' do 32 | run_feature 'features/feature_with_failures.feature' 33 | end 34 | 35 | When 'I run "spinach --backtrace"' do 36 | run_feature 'features/feature_with_failures.feature', append: '--backtrace' 37 | end 38 | 39 | Then 'I should see the failure count along with their messages' do 40 | check_error_messages(1) 41 | @all_stderr.wont_match /gems.*minitest.*assert_equal/ 42 | end 43 | 44 | Then 'I should see the error count along with their messages and backtrace' do 45 | check_error_messages(1) 46 | check_backtrace 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /features/steps/reporting/pending_feature_reporting.rb: -------------------------------------------------------------------------------- 1 | class PendingFeatureReporting < Spinach::FeatureSteps 2 | 3 | feature "Pending feature reporting" 4 | 5 | include Integration::SpinachRunner 6 | 7 | Given "I've written a pending scenario" do 8 | write_file( 9 | 'features/pending_feature.feature', 10 | """ 11 | Feature: A pending feature 12 | 13 | Scenario: This scenario is pending 14 | Given I have a pending step 15 | """) 16 | 17 | write_file( 18 | 'features/steps/pending_feature.rb', 19 | 'class APendingFeature < Spinach::FeatureSteps 20 | 21 | feature "A pending feature" 22 | 23 | Given "I have a pending step" do 24 | pending "This step is pending." 25 | end 26 | end') 27 | 28 | @feature = "features/pending_feature.feature" 29 | end 30 | 31 | Given "I've written a step definition without body" do 32 | write_file( 33 | 'features/pending_feature.feature', 34 | """ 35 | Feature: A pending feature 36 | 37 | Scenario: This scenario is pending 38 | Given this is also a pending step 39 | """) 40 | 41 | write_file( 42 | 'features/steps/pending_feature.rb', 43 | 'class APendingFeature < Spinach::FeatureSteps 44 | 45 | feature "A pending feature" 46 | 47 | Given "this is also a pending step" 48 | end') 49 | 50 | @feature = "features/pending_feature.feature" 51 | end 52 | 53 | When "I run spinach" do 54 | run_feature @feature 55 | end 56 | 57 | Then "I should see a message telling me that there's a pending scenario" do 58 | @stdout.must_include("(1) Pending") 59 | end 60 | 61 | And "I should see a message showing me the reason of the pending scenario" do 62 | @stderr.must_include("This step is pending") 63 | end 64 | 65 | And "I should see a message showing me the default reason of the pending scenario" do 66 | @stderr.must_include("step not implemented") 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /features/steps/reporting/show_step_source_location.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::ShowStepSourceLocation < Spinach::FeatureSteps 2 | include Integration::SpinachRunner 3 | 4 | step "I have a feature that has no error or failure" do 5 | write_file('features/success_feature.feature', """ 6 | Feature: A success feature 7 | 8 | Scenario: This is scenario will succeed 9 | Then I succeed 10 | """) 11 | 12 | write_file('features/steps/success_feature.rb', 13 | 'class Spinach::Features::ASuccessFeature < Spinach::FeatureSteps 14 | feature "A success feature" 15 | step "I succeed" do 16 | end 17 | end') 18 | @feature = "features/success_feature.feature" 19 | end 20 | 21 | step "I run it" do 22 | run_feature @feature 23 | end 24 | 25 | step "I should see the source location of each step of every scenario" do 26 | @stdout.must_match( 27 | /I succeed.*features\/steps\/success_feature\.rb.*3/ 28 | ) 29 | end 30 | 31 | step "I have a feature that has no error or failure and use external steps" do 32 | write_file('features/success_feature.feature', """ 33 | Feature: A feature that uses external steps 34 | 35 | Scenario: This is scenario will succeed 36 | Given this is a external step 37 | """) 38 | 39 | write_file('features/steps/success_feature.rb', 40 | 'class Spinach::Features::AFeatureThatUsesExternalSteps < Spinach::FeatureSteps 41 | feature "A feature that uses external steps" 42 | include ExternalSteps 43 | end') 44 | write_file('features/support/external_steps.rb', 45 | 'module ExternalSteps 46 | include Spinach::DSL 47 | step "this is a external step" do 48 | end 49 | end') 50 | @feature = "features/success_feature.feature" 51 | end 52 | 53 | step "I should see the source location of each step, even external ones" do 54 | @stdout.must_match( 55 | /this is a external step.*features\/support\/external_steps\.rb.*3/ 56 | ) 57 | end 58 | 59 | step "I have a feature that has an error" do 60 | write_file('features/error_feature.feature', """ 61 | Feature: An error feature 62 | 63 | Scenario: This is scenario will not succeed 64 | Then I do not succeed 65 | """) 66 | 67 | write_file('features/steps/error_feature.rb', 68 | 'class Spinach::Features::AnErrorFeature < Spinach::FeatureSteps 69 | feature "An error feature" 70 | step "I do not succeed" do 71 | i_do_not_exist.must_be_equal "Your Mumma" 72 | end 73 | end') 74 | @feature = "features/error_feature.feature" 75 | end 76 | 77 | step "I should see the source location of each step, even ones with errors" do 78 | @stdout.must_match( 79 | /I do not succeed.*features\/steps\/error_feature\.rb.*3/ 80 | ) 81 | end 82 | 83 | step "I have a feature that has a failure" do 84 | write_file('features/failure_feature.feature', """ 85 | Feature: A failure feature 86 | 87 | Scenario: This is scenario will not succeed 88 | Then I do not succeed 89 | """) 90 | 91 | write_file('features/steps/failure_feature.rb', 92 | 'class Spinach::Features::AFailureFeature < Spinach::FeatureSteps 93 | feature "A failure feature" 94 | step "I do not succeed" do 95 | i_exist = "Your Pappa" 96 | i_exist.must_be_equal "Your Mumma" 97 | end 98 | end') 99 | @feature = "features/failure_feature.feature" 100 | end 101 | 102 | step "I should see the source location of each step, even ones with failures" do 103 | @stdout.must_match( 104 | /I do not succeed.*features\/steps\/failure_feature\.rb.*3/ 105 | ) 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /features/steps/reporting/undefined_feature_reporting.rb: -------------------------------------------------------------------------------- 1 | class UndefinedFeatureReporting < Spinach::FeatureSteps 2 | 3 | feature "Undefined feature reporting" 4 | 5 | include Integration::SpinachRunner 6 | 7 | Given "I've written a feature but not its steps" do 8 | write_file('features/feature_without_steps.feature', """ 9 | Feature: Feature without steps 10 | 11 | Scenario: A scenario without steps 12 | Given I have no steps 13 | Then I should do nothing 14 | """) 15 | end 16 | 17 | When "I run spinach" do 18 | run_feature 'features/feature_without_steps.feature' 19 | end 20 | 21 | Then "I should see a message telling me that there's an undefined feature" do 22 | @stderr.must_match /Undefined features.*1/ 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /features/steps/reporting/use_customized_reporter.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | class UseCustomizedReporter < Spinach::FeatureSteps 4 | 5 | feature "Use customized reporter" 6 | 7 | include Integration::SpinachRunner 8 | 9 | before do 10 | class_str = <<-EOF 11 | class Spinach::Reporter::TestReporter < Spinach::Reporter 12 | attr_reader :out, :error 13 | attr_accessor :scenario_error 14 | attr_accessor :scenario 15 | 16 | def initialize(*args) 17 | super(*args) 18 | @out = options[:output] || $stdout 19 | @error = options[:error] || $stderr 20 | @max_step_name_length = 0 21 | end 22 | 23 | def before_feature_run(feature) 24 | out.puts "The customized class" 25 | end 26 | 27 | def before_scenario_run(scenario, step_definitions = nil) 28 | end 29 | 30 | def after_scenario_run(scenario, step_definitions = nil) 31 | end 32 | 33 | def on_successful_step(step, step_location, step_definitions = nil) 34 | end 35 | 36 | def on_failed_step(step, failure, step_location, step_definitions = nil) 37 | end 38 | 39 | def on_error_step(step, failure, step_location, step_definitions = nil) 40 | end 41 | 42 | def on_undefined_step(step, failure, step_definitions = nil) 43 | end 44 | 45 | def on_pending_step(step, failure) 46 | end 47 | 48 | def on_feature_not_found(feature) 49 | end 50 | 51 | def on_skipped_step(step, step_definitions = nil) 52 | end 53 | 54 | def output_step(symbol, step, color, step_location = nil) 55 | end 56 | 57 | def after_run(success) 58 | end 59 | 60 | def run_summary 61 | end 62 | 63 | def full_step(step) 64 | end 65 | 66 | end 67 | EOF 68 | 69 | write_file "test_reporter.rb", class_str 70 | end 71 | 72 | Given 'I have a feature that has no error or failure' do 73 | write_file('features/success_feature.feature', """ 74 | Feature: A success feature 75 | 76 | Scenario: This is scenario will succeed 77 | Then I succeed 78 | """) 79 | 80 | write_file('features/steps/success_feature.rb', 81 | <<-EOF 82 | require_relative "../../test_reporter" 83 | class ASuccessFeature < Spinach::FeatureSteps 84 | feature "A success feature" 85 | Then "I succeed" do 86 | end 87 | end 88 | EOF 89 | ) 90 | @feature = "features/success_feature.feature" 91 | end 92 | 93 | Given 'I have a feature that has one failure' do 94 | write_file('features/failure_feature.feature', """ 95 | Feature: A failure feature 96 | 97 | Scenario: This is scenario will fail 98 | Then I fail 99 | """) 100 | 101 | write_file('features/steps/failure_feature.rb', 102 | <<-EOF 103 | require_relative "../../test_reporter" 104 | class AFailureFeature < Spinach::FeatureSteps 105 | feature "A failure feature" 106 | Then "I fail" do 107 | assert false 108 | end 109 | end 110 | EOF 111 | ) 112 | @feature = "features/failure_feature.feature" 113 | end 114 | 115 | When 'I run it using the new reporter' do 116 | run_feature @feature, append: "-r test_reporter" 117 | end 118 | 119 | Then 'I see the desired output' do 120 | @stdout.must_include("The customized class") 121 | end 122 | 123 | When 'I run it using two custom reporters' do 124 | @failure_filename = "tmp/custom-reporter-#{SecureRandom.hex}.txt" 125 | @expected_path = "tmp/fs/#{@failure_filename}" 126 | run_feature @feature, append: "-r failure_file,test_reporter", env: { 'SPINACH_FAILURE_FILE' => @failure_filename } 127 | end 128 | 129 | Then 'I see one reporter\'s output on the screen' do 130 | @stdout.must_include("The customized class") 131 | end 132 | 133 | Then 'I see the other reporter\'s output in a file' do 134 | assert File.exist?(@expected_path), "Reporter should have created an output file: #{@expected_path}" 135 | File.open(@expected_path).read.must_equal "features/failure_feature.feature:4" 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /features/steps/rspec_compatibility.rb: -------------------------------------------------------------------------------- 1 | class RSpecCompatibility < Spinach::FeatureSteps 2 | 3 | feature "RSpec compatibility" 4 | 5 | include Integration::SpinachRunner 6 | include Integration::ErrorReporting 7 | 8 | Given 'I have a feature that should completely pass' do 9 | write_file('features/feature_without_failures.feature', """ 10 | Feature: Feature without failures 11 | 12 | Scenario: This scenario will pass 13 | Then RSpec expectations and matchers are available 14 | """) 15 | 16 | write_file('features/steps/rspec_feature.rb', 17 | 'class FeatureWithoutFailures < Spinach::FeatureSteps 18 | feature "Feature without failures" 19 | Then "RSpec expectations and matchers are available" do 20 | 42.0.should be_within(0.2).of(42.1) 21 | end 22 | end') 23 | @feature = 'feature_without_failures' 24 | end 25 | 26 | Given "I have a feature with some failed expectations" do 27 | write_file('features/feature_with_failures.feature', """ 28 | Feature: Feature with failures in RSpec compatibility test 29 | 30 | Scenario: This scenario will fail 31 | Given true is false 32 | Then remove all the files in my hard drive""") 33 | 34 | write_file('features/steps/failure_feature.rb', 35 | 'class FeatureWithFailures < Spinach::FeatureSteps 36 | feature "Feature with failures in RSpec compatibility test" 37 | Given "true is false" do 38 | true.should == false 39 | end 40 | 41 | Then "remove all the files in my hard drive" do 42 | # joking! 43 | end 44 | end') 45 | @feature = 'feature_with_failures' 46 | end 47 | 48 | When "I run \"spinach\" with rspec" do 49 | @feature = 50 | run_feature "features/#{@feature}.feature", framework: :rspec 51 | end 52 | 53 | Then "I should see the failure count along with their messages" do 54 | check_error_messages(1) 55 | end 56 | 57 | Given 'I have a sinatra app with some capybara-based expectations' do 58 | write_file("features/support/app.rb", ' 59 | require "sinatra" 60 | require "spinach/capybara" 61 | app = Sinatra::Application.new do 62 | get "/" do 63 | \'Hello world!\' 64 | end 65 | end 66 | Capybara.app = app 67 | ') 68 | 69 | write_file('features/greeting.feature', """ 70 | Feature: Greeting 71 | 72 | Scenario: Greeting 73 | Given I am on the front page 74 | Then I should see hello world 75 | """) 76 | 77 | write_file('features/steps/greeting.rb', 78 | 'require "spinach/capybara" 79 | class Greeting < Spinach::FeatureSteps 80 | Given "I am on the front page" do 81 | visit "/" 82 | end 83 | 84 | Then "I should see hello world" do 85 | page.should have_content("Hello world") 86 | end 87 | end') 88 | @feature = 'greeting' 89 | end 90 | 91 | Given 'There should be no error' do 92 | @last_exit_status.must_equal 0 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /features/steps/running_specific_scenarios.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::RunningSpecificScenarios < Spinach::FeatureSteps 2 | include Integration::SpinachRunner 3 | 4 | Given 'I have a feature with 2 scenarios' do 5 | @feature = Integration::FeatureGenerator.failure_feature_with_two_scenarios 6 | end 7 | 8 | When 'I specify that only the second should be run' do 9 | feature_file = Pathname.new(Filesystem.dirs.first).join(@feature) 10 | step_lines = feature_file.each_line.with_index.select do |line, index| 11 | line.match(/^\s*Then/) 12 | end 13 | 14 | line_number = step_lines[1].last 15 | 16 | run_feature("#{@feature}:#{line_number}") 17 | end 18 | 19 | Then 'One will succeed and none will fail' do 20 | @stdout.must_match("(1) Successful") 21 | @stdout.must_match("(0) Failed") 22 | @stdout.must_match("(0) Pending") 23 | end 24 | 25 | Given 'I have a tagged feature with 2 scenarios' do 26 | @feature = Integration::FeatureGenerator.tagged_failure_feature_with_two_scenarios 27 | end 28 | 29 | When 'I include the tag of the failing scenario' do 30 | run_feature(@feature, {append: "-t @failing"}) 31 | end 32 | 33 | Then 'None will succeed and one will fail' do 34 | @stdout.must_match("(0) Successful") 35 | @stdout.must_match("(1) Failed") 36 | @stdout.must_match("(0) Pending") 37 | end 38 | 39 | When 'I exclude the tag of the passing scenario' do 40 | run_feature(@feature, {append: "-t ~@passing"}) 41 | end 42 | 43 | When 'I include the tag of the feature and exclude the tag of the failing scenario' do 44 | run_feature(@feature, {append: "-t @feature,~@failing"}) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/spec' 3 | require_relative 'filesystem' 4 | 5 | Spinach.hooks.after_scenario do |scenario| 6 | FileUtils.rm_rf(Filesystem.dirs) 7 | end 8 | -------------------------------------------------------------------------------- /features/support/error_reporting.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spinach_runner' 2 | 3 | module Integration 4 | module ErrorReporting 5 | include Filesystem 6 | 7 | def check_error_messages(n = 1) 8 | @stderr.must_match failure_regex(n) 9 | end 10 | 11 | def check_backtrace(n = 1) 12 | @stderr.must_match failure_regex(n) 13 | @stderr.must_match /.*(minitest|rspec).*assert_equal/ 14 | end 15 | 16 | private 17 | 18 | def failure_regex(n) 19 | /Failures \(#{n}\)/ 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /features/support/feature_generator.rb: -------------------------------------------------------------------------------- 1 | module Integration 2 | class FeatureGenerator 3 | class << self 4 | include Filesystem 5 | 6 | # Generate a feature with 1 scenario that should pass 7 | # 8 | # @return feature_filename 9 | # The feature file name 10 | # 11 | # @api private 12 | def success_feature 13 | feature = success_scenario_title + success_scenario 14 | steps = success_step_class_str + success_step + "\nend" 15 | write_feature 'features/success_feature.feature', feature, 16 | 'features/steps/success_feature.rb', steps 17 | end 18 | 19 | # Generate a feature with 1 scenario that has a pending step in between 20 | # 21 | # @return feature_filename 22 | # The feature file name 23 | # 24 | # @api private 25 | def pending_feature_with_multiple_scenario 26 | feature_str = pending_feature_str + "\n" + success_scenario 27 | steps = pending_step_class_str + pending_step + "\n" + failure_step_definition + success_step + "\nend" 28 | write_feature 'features/success_feature.feature', feature_str, 29 | 'features/steps/pending_feature_with_multiple_scenario.rb', steps 30 | end 31 | 32 | # Generate a feature that has 2 scenarios. The first one should 33 | # pass and the second one should fail 34 | # 35 | # @return feature_filename 36 | # The feature file name 37 | # 38 | # @api private 39 | def failure_feature_with_two_scenarios 40 | feature = failure_feature_title + failure_scenario + success_scenario 41 | steps = failure_step + success_step + "\nend" 42 | write_feature failure_filename, feature, 43 | failure_step_filename, steps 44 | end 45 | 46 | # Generate a tagged feature that has 2 tagged scenarios. 47 | # 1 should pass, and 1 should fail. 48 | # 49 | # @return feature_filename 50 | # The feature file name 51 | # 52 | # @api private 53 | def tagged_failure_feature_with_two_scenarios 54 | feature = "@feature\n" + 55 | failure_feature_title + 56 | " @failing" + 57 | failure_scenario + "\n" + 58 | " @passing\n" + 59 | " " + success_scenario 60 | 61 | steps = failure_step + 62 | success_step + "\n" + 63 | "end" 64 | 65 | write_feature( 66 | failure_filename, feature, 67 | failure_step_filename, steps 68 | ) 69 | end 70 | 71 | # Generate a feature with 1 scenario that should fail 72 | # 73 | # @return feature_filename 74 | # The feature file name 75 | # 76 | # @api private 77 | def failure_feature 78 | feature = failure_feature_title + failure_scenario 79 | write_feature failure_filename, feature, 80 | failure_step_filename, failure_step + "\nend" 81 | end 82 | 83 | private 84 | 85 | # Write feature file and its associated step file 86 | # @param feature_file 87 | # The name of the feature file to be written to 88 | # @param feature 89 | # The string to be written into the feature file 90 | # @param step_file 91 | # The name of the step ruby file to be written to 92 | # @param steps 93 | # The string to be written into the step file 94 | # 95 | # @return feature_filename 96 | # The feature file name 97 | # 98 | # @api private 99 | def write_feature(feature_file, feature, step_file, steps) 100 | write_file(feature_file, feature) 101 | write_file(step_file, steps) 102 | feature_file 103 | end 104 | 105 | def failure_step 106 | %Q|class AFailureFeature < Spinach::FeatureSteps 107 | feature "A failure feature" 108 | #{failure_step_definition} 109 | | 110 | end 111 | 112 | def failure_step_definition 113 | 'step "I fail" do 114 | true.must_equal false 115 | end' 116 | end 117 | 118 | def pending_feature_str 119 | "Feature: A feature with pending steps 120 | Scenario: This is scenario will be pending 121 | When this is a pending step 122 | Then I fail" 123 | end 124 | 125 | def success_scenario 126 | 'Scenario: This is scenario will succeed 127 | Then I succeed' 128 | end 129 | 130 | def success_scenario_title 131 | "Feature: A success feature\n\n" 132 | end 133 | 134 | def success_step 135 | ' 136 | step "I succeed" do 137 | end' 138 | end 139 | 140 | def pending_step_class_str 141 | %Q|class ApendingFeature < Spinach::FeatureSteps 142 | feature "A feature with pending steps"\n\n| 143 | end 144 | 145 | def pending_step 146 | ' 147 | step "this is a pending step" do 148 | pending "to be implemented" 149 | end' 150 | end 151 | 152 | def success_step_class_str 153 | %Q|class ASuccessFeature < Spinach::FeatureSteps 154 | feature "A success feature"\n\n| 155 | end 156 | 157 | def failure_step_filename 158 | 'features/steps/failure_feature.rb' 159 | end 160 | 161 | def failure_filename 162 | "features/failure_feature.feature" 163 | end 164 | 165 | def failure_feature_title 166 | "Feature: A failure feature\n\n" 167 | end 168 | 169 | def failure_scenario 170 | " 171 | Scenario: This is scenario will fail 172 | Then I fail\n" 173 | end 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /features/support/filesystem.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'open3' 3 | 4 | # The Filesystem module runs commands, captures their output and exit status 5 | # and lets the host know about it. 6 | # 7 | module Filesystem 8 | def self.dirs 9 | ['tmp/fs'] 10 | end 11 | 12 | # Writes a file with some contents. 13 | # 14 | # @param [String] filename 15 | # The file name to write. 16 | # 17 | # @param [String] contents 18 | # The contents to include in the file. 19 | # 20 | # @api public 21 | def write_file(filename, contents) 22 | in_current_dir do 23 | mkdir(File.dirname(filename)) 24 | File.open(filename, 'w') { |f| f << contents } 25 | end 26 | end 27 | 28 | # Executes a code block within a particular directory. 29 | # 30 | # @param [Proc] block 31 | # The block to execute 32 | # 33 | # @api public 34 | def in_current_dir(&block) 35 | mkdir(current_dir) 36 | Dir.chdir(current_dir, &block) 37 | end 38 | 39 | # Runs a command in the current directory. 40 | # 41 | # It populates the following instance variables: 42 | # 43 | # * @stdout - The standard output captured from the process. 44 | # * @stderr - The standard error captured from the process. 45 | # * @last_exit_status - The process exit status. 46 | # 47 | # @param [String] command 48 | # The command to run. 49 | # @param [Hash] env 50 | # Hash of environment variables to use with command 51 | # 52 | # @api public 53 | def run(command, env = nil) 54 | in_current_dir do 55 | args = command.strip.split(" ") 56 | args = args.unshift(env) if env 57 | @stdout, @stderr, @last_exit_status = Open3.capture3(*args) 58 | end 59 | 60 | @stdout = strip_colors(@stdout) 61 | @stderr = strip_colors(@stderr) 62 | end 63 | 64 | private 65 | 66 | def mkdir(dirname) 67 | FileUtils.mkdir_p(dirname) unless File.directory?(dirname) 68 | end 69 | 70 | def rmdir(dirname) 71 | FileUtils.rm_rf(dirname) unless File.directory?(dirname) 72 | end 73 | 74 | def current_dir 75 | File.join(*dirs) 76 | end 77 | 78 | def dirs 79 | ['tmp/fs'] 80 | end 81 | 82 | def strip_colors(string) 83 | string.gsub(/\e\[((\d;?)+)m/, "") 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /features/support/spinach_runner.rb: -------------------------------------------------------------------------------- 1 | require "rbconfig" 2 | require_relative 'filesystem' 3 | 4 | module Integration 5 | module SpinachRunner 6 | include Filesystem 7 | 8 | def self.included(base) 9 | Spinach.hooks.before_scenario do 10 | if respond_to?(:in_current_dir) 11 | in_current_dir do 12 | rmdir "rails_app" 13 | end 14 | end 15 | end 16 | end 17 | 18 | def run_feature(feature, options={}) 19 | options[:framework] ||= :minitest 20 | use_minitest if options[:framework] == :minitest 21 | use_rspec if options[:framework] == :rspec 22 | spinach = File.expand_path("bin/spinach") 23 | run "#{ruby} #{spinach} #{feature} #{options[:append]}", options[:env] 24 | end 25 | 26 | def run_spinach(options = {}) 27 | options[:framework] ||= :minitest 28 | 29 | use_minitest if options[:framework] == :minitest 30 | use_rspec if options[:framework] == :rspec 31 | 32 | spinach = File.expand_path("bin/spinach") 33 | 34 | run "#{ruby} #{spinach} #{options[:append]}" 35 | end 36 | 37 | def ruby 38 | return @ruby if defined?(@ruby) 39 | 40 | config = RbConfig::CONFIG 41 | @ruby = File.join(config["bindir"], 42 | "#{config["ruby_install_name"]}#{config["EXEEXT"]}") 43 | end 44 | 45 | def use_minitest 46 | write_file('features/support/minitest.rb', 47 | "require 'minitest/spec'") 48 | end 49 | 50 | def use_rspec 51 | write_file('features/support/minitest.rb', 52 | "require 'rspec/core'\nrequire 'rspec/expectations'") 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/spinach.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spinach/version' 2 | require_relative 'spinach/config' 3 | require_relative 'spinach/hookable' 4 | require_relative 'spinach/hooks' 5 | require_relative 'spinach/support' 6 | require_relative 'spinach/exceptions' 7 | require_relative 'spinach/runner' 8 | require_relative 'spinach/parser' 9 | require_relative 'spinach/dsl' 10 | require_relative 'spinach/feature_steps' 11 | require_relative 'spinach/reporter' 12 | require_relative 'spinach/orderers' 13 | require_relative 'spinach/cli' 14 | require_relative 'spinach/generators' 15 | require_relative 'spinach/auditor' 16 | 17 | require_relative 'spinach/background' 18 | require_relative 'spinach/feature' 19 | require_relative 'spinach/features' 20 | require_relative 'spinach/scenario' 21 | require_relative 'spinach/step' 22 | 23 | # Spinach is a BDD framework leveraging the great GherkinRuby language. This 24 | # language is the one used defining features in Cucumber, the BDD framework 25 | # Spinach is inspired upon. 26 | # 27 | # Its main design goals are: 28 | # 29 | # * No magic: All features are implemented using normal Ruby classes. 30 | # * Reusability: Steps are methods, so they can be reused using modules, a 31 | # common, normal practice among rubyists. 32 | # * Proper encapsulation: No conflicts between steps from different 33 | # scenarios. 34 | # 35 | module Spinach 36 | @@feature_steps = [] 37 | 38 | # @return [Array] 39 | # All the registered features. 40 | # 41 | # @api public 42 | def self.feature_steps 43 | @@feature_steps 44 | end 45 | 46 | # Resets Spinach to a pristine state, as if no feature was ever registered. 47 | # Mostly useful in Spinach's own testing. 48 | # 49 | # @api semipublic 50 | def self.reset_feature_steps 51 | @@feature_steps = [] 52 | end 53 | 54 | # Returns a new hook object that will receive all the messages from the run 55 | # and fire up the appropiate callbacks when needed. 56 | # 57 | def self.hooks 58 | @@hooks ||= Hooks.new 59 | end 60 | 61 | # Finds step definitions given a feature name. 62 | # 63 | # @param [String] name 64 | # The feature name to get the definitions for. 65 | # 66 | # @return [StepDefinitions] 67 | # the {StepDefinitions} class for the given feature name 68 | # 69 | # @api public 70 | def self.find_step_definitions(name) 71 | klass = Spinach::Support.camelize(name) 72 | scoped_klass = Spinach::Support.scoped_camelize(name) 73 | feature_steps.detect do |feature| 74 | feature.name == klass || 75 | feature.name == scoped_klass || 76 | feature.feature_name.to_s == name.to_s 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/spinach/auditor.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Spinach 4 | # The auditor audits steps and determines if any are missing or obsolete. 5 | # 6 | # It is a subclass of Runner because it uses many of the Runner's features 7 | # when auditing. 8 | # 9 | class Auditor < Runner 10 | attr_accessor :unused_steps, :used_steps 11 | 12 | def initialize(filenames) 13 | super(filenames) 14 | @unused_steps = {} 15 | @used_steps = Set.new 16 | end 17 | 18 | # audits features 19 | # @files [Array] 20 | # filenames to audit 21 | def run 22 | require_dependencies 23 | 24 | # Find any missing steps in each file, and keep track of unused steps 25 | clean = true 26 | filenames.each do |file| 27 | result = audit_file(file) 28 | clean &&= result # set to false if any result is false 29 | end 30 | 31 | # At the end, report any unused steps 32 | report_unused_steps 33 | 34 | # If the audit was clean, make sure the user knows 35 | puts "\nAudit clean - no missing steps.".colorize(:light_green) if clean 36 | 37 | true 38 | end 39 | 40 | private 41 | 42 | def audit_file(file) 43 | puts "\nAuditing: ".colorize(:magenta) + file.colorize(:light_magenta) 44 | 45 | # Find the feature definition and its associated step defs class 46 | feature, step_defs_class = get_feature_and_defs(file) 47 | return step_file_missing if step_defs_class.nil? 48 | step_defs = step_defs_class.new 49 | unused_step_names = step_names_for_class(step_defs_class) 50 | 51 | missing_steps = {} 52 | 53 | feature.each_step do |step| 54 | # Audit the step 55 | missing_steps[step.name] = step if step_missing?(step, step_defs) 56 | # Having audited the step, remove it from the list of unused steps 57 | unused_step_names.delete step.name 58 | end 59 | 60 | # If there are any steps left at the end, let's mark them as unused 61 | store_unused_steps(unused_step_names, step_defs) 62 | 63 | # And then generate a report of missing steps 64 | return true if missing_steps.empty? 65 | report_missing_steps(missing_steps.values) 66 | false 67 | end 68 | 69 | # Get the feature and its definitions from the appropriate files 70 | def get_feature_and_defs(file) 71 | feature = Parser.open_file(file).parse 72 | [feature, Spinach.find_step_definitions(feature.name)] 73 | end 74 | 75 | # Process a step from the feature file using the given step_defs. 76 | # If it is missing, return true. Otherwise, add it to the used_steps for 77 | # the report at the end and return false. 78 | def step_missing?(step, step_defs) 79 | method_name = Spinach::Support.underscore step.name 80 | return true unless step_defs.respond_to?(method_name) 81 | # Remember that we have used this step 82 | used_steps << step_defs.step_location_for(step.name).join(':') 83 | false 84 | end 85 | 86 | # Store any unused step names for the report at the end of the audit 87 | def store_unused_steps(names, step_defs) 88 | names.each do |name| 89 | location = step_defs.step_location_for(name).join(':') 90 | unused_steps[location] = name 91 | end 92 | end 93 | 94 | # Print a message alerting the user that there is no step file for this 95 | # feature 96 | def step_file_missing 97 | puts 'Step file missing: please run --generate first!' 98 | .colorize(:light_red) 99 | false 100 | end 101 | 102 | # Get the step names for all steps in the given class, including those in 103 | # common modules 104 | def step_names_for_class(klass) 105 | klass.ancestors.map { |a| a.respond_to?(:steps) ? a.steps : [] }.flatten 106 | end 107 | 108 | # Produce a report of unused steps that were not found anywhere in the audit 109 | def report_unused_steps 110 | # Remove any unused_steps that were in common modules and used 111 | # in another feature 112 | used_steps.each { |location| unused_steps.delete location } 113 | unused_steps.each do |location, name| 114 | puts "\n" + "Unused step: #{location} ".colorize(:yellow) + 115 | "'#{name}'".colorize(:light_yellow) 116 | end 117 | end 118 | 119 | # Print a report of the missing step objects provided 120 | def report_missing_steps(steps) 121 | puts "\nMissing steps:".colorize(:light_cyan) 122 | steps.each do |step| 123 | puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' ') 124 | .colorize(:cyan) 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/spinach/background.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | class Background 3 | attr_accessor :line 4 | attr_accessor :steps, :feature 5 | 6 | def initialize(feature) 7 | @feature = feature 8 | @steps = [] 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/spinach/capybara.rb: -------------------------------------------------------------------------------- 1 | require 'capybara' 2 | require 'capybara/dsl' 3 | require 'rbconfig' 4 | require 'spinach/config' 5 | require_relative 'feature_steps' 6 | 7 | module Spinach 8 | class FeatureSteps 9 | # Spinach's capybara module makes Capybara DSL available in all features. 10 | # 11 | # @example 12 | # require 'spinach/capybara' 13 | # class CapybaraFeature < Spinach::FeatureSteps 14 | # Given "I go to the home page" do 15 | # visit '/' 16 | # end 17 | # end 18 | # 19 | module Capybara 20 | include ::Capybara::DSL 21 | 22 | if defined?(RSpec) 23 | require 'rspec/matchers' 24 | require 'capybara/rspec' 25 | include ::Capybara::RSpecMatchers 26 | end 27 | end 28 | end 29 | end 30 | 31 | Spinach.hooks.after_scenario do 32 | ::Capybara.reset_sessions! if ::Capybara.app 33 | ::Capybara.use_default_driver 34 | end 35 | 36 | Spinach.hooks.on_tag('javascript') do 37 | ::Capybara.current_driver = ::Capybara.javascript_driver 38 | end 39 | 40 | open_page = Proc.new do |*args| 41 | if Spinach.config.save_and_open_page_on_failure 42 | step_definitions = args.last 43 | step_definitions.send(:save_and_open_page) 44 | end 45 | end 46 | 47 | Spinach.hooks.on_error_step(&open_page) 48 | Spinach.hooks.on_failed_step(&open_page) 49 | 50 | Spinach::FeatureSteps.send :include, Spinach::FeatureSteps::Capybara 51 | -------------------------------------------------------------------------------- /lib/spinach/cli.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | module Spinach 4 | # The cli is a class responsible of handling all the command line interface 5 | # logic. 6 | # 7 | class Cli 8 | # @param [Array] arguments 9 | # The command line arguments 10 | # 11 | # @api public 12 | def initialize(args = ARGV) 13 | @args = args 14 | end 15 | 16 | # Runs all the features. 17 | # 18 | # @return [true, false] 19 | # The exit status - true for success, false for failure. 20 | # 21 | # @api public 22 | def run 23 | options 24 | 25 | if Spinach.config.generate 26 | Spinach::Generators.run(feature_files) 27 | elsif Spinach.config.audit 28 | Spinach::Auditor.new(feature_files).run 29 | else 30 | Spinach::Runner.new(feature_files).run 31 | end 32 | end 33 | 34 | # @return [Hash] 35 | # A hash of options separated by its type. 36 | # 37 | # @example 38 | # Cli.new.options 39 | # # => { reporter: { backtrace: true } } 40 | # 41 | # @api public 42 | def options 43 | @options ||= parse_options 44 | end 45 | 46 | # Uses given args to list the feature files to run. It will find a single 47 | # feature, features in a folder and subfolders or every feature file in the 48 | # feature path. 49 | # 50 | # @return [Array] 51 | # An array with the feature file names. 52 | # 53 | # @api public 54 | def feature_files 55 | files_to_run = [] 56 | 57 | @args.each do |arg| 58 | if arg.match(/\.feature/) 59 | if File.exist? arg.gsub(/:\d*/, '') 60 | files_to_run << arg 61 | else 62 | fail! "#{arg} could not be found" 63 | end 64 | elsif File.directory?(arg) 65 | files_to_run << Dir.glob(File.join(arg, '**', '*.feature')) 66 | elsif arg != "{}" 67 | fail! "invalid argument - #{arg}" 68 | end 69 | end 70 | 71 | if !files_to_run.empty? 72 | files_to_run.flatten 73 | else 74 | Dir.glob(File.join(Spinach.config[:features_path], '**', '*.feature')) 75 | end 76 | end 77 | 78 | private 79 | 80 | # Parses the arguments into options. 81 | # 82 | # @return [Hash] 83 | # A hash of options separated by its type. 84 | # 85 | # @api private 86 | def parse_options 87 | config = {} 88 | 89 | begin 90 | OptionParser.new do |opts| 91 | opts.on('-c', '--config_path PATH', 92 | 'Parse options from file (will get overriden by flags)') do |file| 93 | Spinach.config[:config_path] = file 94 | end 95 | 96 | opts.on('-b', '--backtrace', 97 | 'Show backtrace of errors') do |show_backtrace| 98 | config[:reporter_options] = {backtrace: show_backtrace} 99 | end 100 | 101 | opts.on('-t', '--tags TAG', 102 | 'Run all scenarios for given tags.') do |tag| 103 | config[:tags] ||= [] 104 | tags = tag.delete('@').split(',') 105 | 106 | if (config[:tags] + tags).flatten.none? { |t| t =~ /wip$/ } 107 | tags.unshift '~wip' 108 | end 109 | 110 | config[:tags] << tags 111 | end 112 | 113 | opts.on('-g', '--generate', 114 | 'Auto-generate the feature steps files') do 115 | config[:generate] = true 116 | end 117 | 118 | opts.on_tail('--version', 'Show version') do 119 | puts Spinach::VERSION 120 | exit 121 | end 122 | 123 | opts.on('-f', '--features_path PATH', 124 | 'Path where your features will be searched for') do |path| 125 | config[:features_path] = path 126 | end 127 | 128 | opts.on('-r', '--reporter CLASS_NAMES', 129 | 'Formatter class names, separated by commas') do |class_names| 130 | names = class_names.split(',').map { |c| reporter_class(c) } 131 | config[:reporter_classes] = names 132 | end 133 | 134 | opts.on('--rand', "Randomize the order of features and scenarios") do 135 | config[:orderer_class] = orderer_class(:random) 136 | end 137 | 138 | opts.on('--seed SEED', Integer, 139 | "Provide a seed for randomizing the order of features and scenarios") do |seed| 140 | config[:orderer_class] = orderer_class(:random) 141 | config[:seed] = seed 142 | end 143 | 144 | opts.on_tail('--fail-fast', 145 | 'Terminate the suite run on the first failure') do |class_name| 146 | config[:fail_fast] = true 147 | end 148 | 149 | opts.on('-a', '--audit', 150 | "Audit steps instead of running them, outputting missing \ 151 | and obsolete steps") do 152 | config[:audit] = true 153 | end 154 | end.parse!(@args) 155 | 156 | Spinach.config.parse_from_file 157 | config.each{|k,v| Spinach.config[k] = v} 158 | if Spinach.config.tags.empty? || 159 | Spinach.config.tags.flatten.none?{ |t| t =~ /wip$/ } 160 | Spinach.config.tags.unshift ['~wip'] 161 | end 162 | rescue OptionParser::ParseError => exception 163 | puts exception.message.capitalize 164 | exit 1 165 | end 166 | end 167 | 168 | # exit test run with an optional message to the user 169 | def fail!(message=nil) 170 | puts message if message 171 | exit 1 172 | end 173 | 174 | # Builds the class name to use an output reporter. 175 | # 176 | # @param [String] klass 177 | # The class name of the reporter. 178 | # 179 | # @return [String] 180 | # The full name of the reporter class. 181 | # 182 | # @example 183 | # reporter_class('progress') 184 | # # => Spinach::Reporter::Progress 185 | # 186 | # @api private 187 | def reporter_class(klass) 188 | "Spinach::Reporter::" + Spinach::Support.camelize(klass) 189 | end 190 | 191 | # Builds the class to use an orderer. 192 | # 193 | # @param [String] klass 194 | # The class name of the orderer. 195 | # 196 | # @return [String] 197 | # The full name of the orderer class. 198 | # 199 | # @example 200 | # orderer_class('random') 201 | # # => Spinach::Orderers::Random 202 | # 203 | # @api private 204 | def orderer_class(klass) 205 | "Spinach::Orderers::" + Spinach::Support.camelize(klass) 206 | end 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /lib/spinach/config.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | module Spinach 4 | # Accesses spinach config. Allows you to configure several runtime options, 5 | # like the step definitions path. 6 | # 7 | # @return [Config] 8 | # The config object 9 | # 10 | # @example 11 | # Spinach.config[:step_definitions_path] 12 | # # => 'features/steps' 13 | # Spinach.config[:step_definitions_path] = 'integration/steps' 14 | # # => 'integration/steps' 15 | # 16 | # @api public 17 | def self.config 18 | @config ||= Config.new 19 | end 20 | 21 | # The config object holds all the runtime configurations needed for spinach 22 | # to run. 23 | # 24 | class Config 25 | attr_writer :features_path, 26 | :step_definitions_path, 27 | :default_reporter, 28 | :support_path, 29 | :failure_exceptions, 30 | :config_path, 31 | :tags, 32 | :generate, 33 | :save_and_open_page_on_failure, 34 | :reporter_classes, 35 | :reporter_options, 36 | :orderer_class, 37 | :seed, 38 | :fail_fast, 39 | :audit 40 | 41 | 42 | # The "features path" holds the place where your features will be 43 | # searched for. Defaults to 'features' 44 | # 45 | # @return [String] 46 | # The features path. 47 | # 48 | # @api public 49 | def features_path 50 | @features_path || 'features' 51 | end 52 | 53 | # The "reporter classes" holds an array of reporter class name 54 | # Default to ["Spinach::Reporter::Stdout"] 55 | # 56 | # @return [Array] 57 | # The reporters that respond to specific messages. 58 | # 59 | # @api public 60 | def reporter_classes 61 | @reporter_classes || ["Spinach::Reporter::Stdout"] 62 | end 63 | 64 | # The "reporter_options" holds the options passed to reporter_classes 65 | # 66 | # @api public 67 | def reporter_options 68 | @reporter_options || {} 69 | end 70 | 71 | # The "orderer class" holds the orderer class name 72 | # Defaults to Spinach::Orderers::Default 73 | # 74 | # @return [orderer object] 75 | # The orderer that responds to specific messages. 76 | # 77 | # @api public 78 | def orderer_class 79 | @orderer_class || "Spinach::Orderers::Default" 80 | end 81 | 82 | # A randomization seed. This is what spinach uses for test run 83 | # randomization, so if you call `Kernel.srand(Spinach.config.seed)` 84 | # in your support environment file, not only will the test run 85 | # order be guaranteed to be stable under a specific seed, all 86 | # the Ruby-generated random numbers produced during your test 87 | # run will also be stable under that seed. 88 | # 89 | # @api public 90 | def seed 91 | @seed ||= rand(0xFFFF) 92 | end 93 | 94 | # The "step definitions path" holds the place where your feature step 95 | # classes will be searched for. Defaults to '#{features_path}/steps' 96 | # 97 | # @return [String] 98 | # The step definitions path. 99 | # 100 | # @api public 101 | def step_definitions_path 102 | @step_definitions_path || "#{self.features_path}/steps" 103 | end 104 | 105 | # The "support path" helds the place where you can put your configuration 106 | # files. Defaults to '#{features_path}/support' 107 | # 108 | # @return [String] 109 | # The support file path. 110 | # 111 | # @api public 112 | def support_path 113 | @support_path || "#{self.features_path}/support" 114 | end 115 | 116 | def generate 117 | @generate || false 118 | end 119 | 120 | # Allows you to read the config object using a hash-like syntax. 121 | # 122 | # @param [String] attribute 123 | # The attribute to fetch. 124 | # 125 | # @example 126 | # Spinach.config[:step_definitions_path] 127 | # # => 'features/steps' 128 | # 129 | # @api public 130 | def [](attribute) 131 | self.send(attribute) 132 | end 133 | 134 | # Allows you to set config properties using a hash-like syntax. 135 | # 136 | # @param [#to_s] attribute 137 | # The attribute to set. 138 | # 139 | # @param [Object] value 140 | # The value to set the attribute to. 141 | # 142 | # @example 143 | # Spinach.config[:step_definitions_path] = 'integration/steps' 144 | # # => 'integration/steps' 145 | # 146 | # @api public 147 | def []=(attribute, value) 148 | self.send("#{attribute}=", value) 149 | end 150 | 151 | # The failure exceptions return an array of exceptions to be captured and 152 | # considered as failures (as opposite of errors) 153 | # 154 | # @return [Array] 155 | # 156 | # @api public 157 | def failure_exceptions 158 | @failure_exceptions ||= [] 159 | end 160 | 161 | # The "fail_fast" determines if the suite run should exit 162 | # when encountering a failure/error 163 | # 164 | # @return [true/false] 165 | # The fail_fast flag. 166 | # 167 | # @api public 168 | def fail_fast 169 | @fail_fast 170 | end 171 | 172 | # "audit" enables step auditing mode 173 | # 174 | # @return [true/false] 175 | # The audit flag. 176 | # 177 | # @api public 178 | def audit 179 | @audit || false 180 | end 181 | 182 | # It allows you to set a config file to parse for all the other options to be set 183 | # 184 | # @return [String] 185 | # The config file name 186 | # 187 | def config_path 188 | @config_path ||= 'config/spinach.yml' 189 | end 190 | 191 | # When using capybara, it automatically shows the current page when there's 192 | # a failure 193 | # 194 | def save_and_open_page_on_failure 195 | @save_and_open_page_on_failure ||= false 196 | end 197 | 198 | # Tags to tell Spinach that you only want to run scenarios that have (or 199 | # don't have) certain tags. 200 | # 201 | # @return [Array] 202 | # The tags. 203 | # 204 | def tags 205 | @tags ||= [] 206 | end 207 | 208 | # Parse options from the config file 209 | # 210 | # @return [Boolean] 211 | # If the config was parsed from the file 212 | # 213 | def parse_from_file 214 | parsed_opts = YAML.load_file(config_path) 215 | parsed_opts.delete_if{|k| k.to_s == 'config_path'} 216 | parsed_opts.each_pair{|k,v| self[k] = v} 217 | true 218 | rescue Errno::ENOENT 219 | false 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /lib/spinach/dsl.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # Spinach DSL aims to provide an easy way to define steps and hooks into your 3 | # feature classes. 4 | # 5 | module DSL 6 | # @param [Class] base 7 | # The host class. 8 | # 9 | # @api public 10 | def self.included(base) 11 | base.class_eval do 12 | include InstanceMethods 13 | extend ClassMethods 14 | end 15 | end 16 | 17 | # Class methods to extend the host class. 18 | # 19 | module ClassMethods 20 | # The feature name. 21 | attr_reader :feature_name 22 | # Defines an action to perform given a particular step literal. 23 | # 24 | # @param [String] step 25 | # The step name. 26 | # 27 | # @param [Proc] block 28 | # Action to perform in that step. 29 | # If no block is given, step will be defined as pending. 30 | # 31 | # @example 32 | # These 3 examples are equivalent: 33 | # 34 | # class MyFeature < Spinach::FeatureSteps 35 | # When "I go to the toilet" do 36 | # @sittin_on_the_toilet.must_equal true 37 | # end 38 | # end 39 | # 40 | # class MyFeature < Spinach::FeatureSteps 41 | # step "I go to the toilet" do 42 | # @sittin_on_the_toilet.must_equal true 43 | # end 44 | # end 45 | # 46 | # class MyFeature < Spinach::FeatureSteps 47 | # def i_go_to_the_toilet 48 | # @sittin_on_the_toilet.must_equal true 49 | # end 50 | # end 51 | # 52 | # @api public 53 | def step(step, &block) 54 | method_body = if block_given? then block 55 | else lambda { pending "step not implemented" } 56 | end 57 | 58 | define_method(Spinach::Support.underscore(step), &method_body) 59 | steps << step 60 | end 61 | 62 | alias_method :Given, :step 63 | alias_method :When, :step 64 | alias_method :Then, :step 65 | alias_method :And, :step 66 | alias_method :But, :step 67 | 68 | # Defines a before hook for each scenario. The scope is limited only to the current 69 | # step class (thus the current feature). 70 | # 71 | # When a scenario is executed, the before each block will be run first before any steps 72 | # 73 | # User can define multiple before blocks throughout the class hierarchy and they are chained 74 | # through the inheritance chain when executing 75 | # 76 | # @example 77 | # 78 | # class MySpinach::Base< Spinach::FeatureSteps 79 | # before do 80 | # @var1 = 30 81 | # @var2 = 40 82 | # end 83 | # end 84 | # 85 | # class MyFeature < MySpinach::Base 86 | # before do 87 | # self.original_session_timeout = 1000 88 | # change_session_timeout_to(1) 89 | # @var2 = 50 90 | # end 91 | # end 92 | # 93 | # When running a scenario in MyFeature, @var1 is 30 and @var2 is 50 94 | # 95 | # @api public 96 | def before(&block) 97 | define_before_or_after_method_with_block(:before, &block) 98 | end 99 | 100 | # Defines a after hook for each scenario. The scope is limited only to the current 101 | # step class (thus the current feature). 102 | # 103 | # When a scenario is executed, the after each block will be run after any steps 104 | # 105 | # User can define multiple after blocks throughout the class hierarchy and they are chained 106 | # through the inheritance chain when executing. 107 | # 108 | # @example 109 | # 110 | # class MySpinach::Base < Spinach::FeatureSteps 111 | # after do 112 | # @var1 = 30 113 | # @var2 = 40 114 | # end 115 | # end 116 | # 117 | # class MyFeature < MySpinach::Base 118 | # after do 119 | # change_session_timeout_to(original_session_timeout) 120 | # @var2 = 50 121 | # end 122 | # end 123 | # 124 | # When running a scenario in MyFeature, @var1 is 30 and @var2 is 50 125 | # 126 | # @api public 127 | def after(&block) 128 | define_before_or_after_method_with_block(:after, &block) 129 | end 130 | 131 | # Sets the feature name. 132 | # 133 | # @param [String] name 134 | # The name. 135 | # 136 | # @example 137 | # class MyFeature < Spinach::FeatureSteps 138 | # feature "Satisfy needs" 139 | # end 140 | # 141 | # @api public 142 | def feature(name) 143 | @feature_name = name 144 | end 145 | 146 | # Get the list of step names in this class 147 | def steps 148 | @steps ||= [] 149 | end 150 | 151 | private 152 | 153 | def before_or_after_private_method_name(location) 154 | hash_value = hash 155 | class_name = self.name || "" 156 | class_name = class_name.gsub("::", "__").downcase 157 | private_method_name = "_#{location}_each_block_#{hash.abs}_#{class_name}" #uniqueness 158 | end 159 | 160 | def define_before_or_after_method_with_block(location, &block) 161 | define_method(before_or_after_private_method_name(location), &block) 162 | private before_or_after_private_method_name(location) 163 | private_method_name = before_or_after_private_method_name location 164 | define_method "#{location}_each" do 165 | super() 166 | send(private_method_name) 167 | end 168 | end 169 | 170 | end 171 | 172 | # Instance methods to include in the host class. 173 | # 174 | module InstanceMethods 175 | # Executes a given step. 176 | # 177 | # @api public 178 | def execute(step) 179 | underscored_step = Spinach::Support.underscore(step.name) 180 | if self.respond_to?(underscored_step) 181 | self.send(underscored_step) 182 | else 183 | raise Spinach::StepNotDefinedException.new(step) 184 | end 185 | end 186 | 187 | # Gets current step source location. 188 | # 189 | # @param [String] step 190 | # The step name to execute. 191 | # 192 | # @return [String] 193 | # The file and line where the step was defined. 194 | def step_location_for(step) 195 | underscored_step = Spinach::Support.underscore(step) 196 | location = method(underscored_step).source_location if self.respond_to?(underscored_step) 197 | end 198 | 199 | # @return [String] 200 | # The feature name. 201 | def name 202 | self.class.feature_name 203 | end 204 | 205 | # Raises an exception that defines the current step as a pending one. 206 | # 207 | # @api public 208 | # 209 | # @param [String] reason 210 | # The reason why the step is set to pending 211 | # 212 | # @raise [Spinach::StepPendingException] 213 | # Raising the exception tells the scenario runner the current step is 214 | # pending. 215 | def pending(reason) 216 | raise Spinach::StepPendingException.new(reason) 217 | end 218 | 219 | end 220 | end 221 | end 222 | -------------------------------------------------------------------------------- /lib/spinach/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # This class represents the exception raised when Spinach can't find a step 3 | # for a {FeatureSteps}. 4 | # 5 | class StepNotDefinedException < StandardError 6 | attr_reader :step 7 | 8 | # @param [Hash] step 9 | # The missing step. 10 | # 11 | # @api public 12 | def initialize(step) 13 | @step = step 14 | end 15 | 16 | # @return [String] 17 | # A custom message when scenario steps aren't found. 18 | # 19 | # @api public 20 | def message 21 | "Step '#{@step.name}' not found" 22 | end 23 | end 24 | 25 | # This class represents the exception raised when Spinach find a step 26 | # which claims to be pending for a {FeatureSteps}. 27 | # 28 | class StepPendingException < StandardError 29 | attr_reader :reason 30 | attr_accessor :step 31 | 32 | # @param [String] reason 33 | # The reason why the step is set to pending 34 | # 35 | # @api public 36 | def initialize(reason) 37 | @reason = reason 38 | end 39 | 40 | # @return [String] 41 | # A custom message when scenario steps are pending. 42 | # 43 | # @api public 44 | def message 45 | "Step '#{@step.name}' pending" 46 | end 47 | end 48 | 49 | # This class represents the exception raised when Spinach detects 50 | # that the around_scenario hook does not yield. 51 | # 52 | class HookNotYieldException < StandardError 53 | attr_reader :hook 54 | 55 | # @param [String] hook 56 | # The hook which did not yield 57 | # 58 | # @api public 59 | def initialize(hook) 60 | @hook = hook 61 | end 62 | 63 | # @return [String] 64 | # A custom message when a hook did not yield. 65 | # 66 | # @api public 67 | def message 68 | "#{@hook} hooks *must* yield" 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/spinach/feature.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | class Feature 3 | attr_accessor :filename 4 | attr_accessor :name, :scenarios, :tags 5 | attr_accessor :background 6 | attr_accessor :description 7 | attr_reader :lines_to_run 8 | 9 | def initialize 10 | @scenarios = [] 11 | @tags = [] 12 | @lines_to_run = [] 13 | end 14 | 15 | def background_steps 16 | @background.nil? ? [] : @background.steps 17 | end 18 | 19 | def lines_to_run=(lines) 20 | @lines_to_run = lines.map(&:to_i) if lines && lines.any? 21 | end 22 | 23 | def run_every_scenario? 24 | lines_to_run.empty? 25 | end 26 | 27 | # Identifier used by orderers. 28 | # 29 | # Needs to involve the relative file path so that the ordering 30 | # a seed generates is stable across both runs and machines. 31 | # 32 | # @api public 33 | alias ordering_id filename 34 | 35 | # Run the provided code for every step 36 | def each_step 37 | scenarios.each { |scenario| scenario.steps.each { |step| yield step } } 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/spinach/feature_steps.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # The feature class is the class which all the features must inherit from. 3 | # 4 | class FeatureSteps 5 | include DSL 6 | 7 | class << self 8 | # Registers the feature class for later use. 9 | # 10 | # @param [Class] base 11 | # The host class. 12 | # 13 | # @api public 14 | def inherited(base) 15 | Spinach.feature_steps << base 16 | end 17 | 18 | alias_method :include_private, :include 19 | 20 | # Exposes the include method publicly so you can add more modules to it 21 | # due its plastic nature. 22 | # 23 | # @example 24 | # Spinach::FeatureSteps.include Capybara::DSL 25 | def include(*args) 26 | include_private(*args) 27 | end 28 | end 29 | 30 | def before_each; end 31 | def after_each; end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/spinach/features.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # This module provides scoping to user genereated features, so they won't 3 | # conflict with the application code. 4 | module Features 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/spinach/frameworks.rb: -------------------------------------------------------------------------------- 1 | require_relative 'frameworks/minitest' if defined?(MiniTest) 2 | require_relative 'frameworks/rspec' if defined?(RSpec::Expectations) 3 | -------------------------------------------------------------------------------- /lib/spinach/frameworks/minitest.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | MiniTest::Spec.new nil if defined?(MiniTest::Spec) 3 | Spinach.config[:failure_exceptions] << MiniTest::Assertion 4 | 5 | class Spinach::FeatureSteps 6 | include MiniTest::Assertions 7 | attr_accessor :assertions 8 | 9 | def initialize(*args) 10 | super *args 11 | self.assertions = 0 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/spinach/frameworks/rspec.rb: -------------------------------------------------------------------------------- 1 | Spinach.config[:failure_exceptions] << RSpec::Expectations::ExpectationNotMetError 2 | Spinach::FeatureSteps.include RSpec::Matchers 3 | -------------------------------------------------------------------------------- /lib/spinach/generators.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # Spinach generators are a set of utils that auto-generates example features 3 | # given some parsed feature data. 4 | # 5 | module Generators 6 | # generates steps for features 7 | # @files [Array] 8 | # filenames to evaluate for step generation 9 | def self.run(files) 10 | successful = true 11 | files.each do |file| 12 | feature = Parser.open_file(file).parse 13 | 14 | begin 15 | FeatureGenerator.new(feature).store 16 | rescue FeatureGeneratorException => e 17 | successful = false 18 | $stderr.puts e 19 | end 20 | end 21 | successful 22 | end 23 | end 24 | end 25 | 26 | require_relative 'generators/feature_generator' 27 | require_relative 'generators/step_generator' 28 | -------------------------------------------------------------------------------- /lib/spinach/generators/feature_generator.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | module Spinach 4 | module Generators 5 | # A feature generator generates and/or writes an example feature steps class 6 | # given the parsed feture data 7 | class FeatureGenerator 8 | 9 | attr_reader :feature 10 | 11 | # @param [Feature] feature 12 | # The feature returned from the {Parser} 13 | def initialize(feature) 14 | @feature = feature 15 | end 16 | 17 | # @return [Array] 18 | # an array of unique steps found in this feature, avoiding name 19 | # repetition 20 | def steps 21 | scenario_steps = @feature.scenarios.map(&:steps).flatten 22 | background_steps = @feature.background_steps 23 | 24 | (scenario_steps + background_steps).uniq(&:name) 25 | end 26 | 27 | # @return [String] 28 | # this feature's name 29 | def name 30 | @feature.name 31 | end 32 | 33 | # @return [String] 34 | # an example feature steps definition 35 | def generate 36 | result = StringIO.new 37 | result.puts "class #{Spinach::Support.scoped_camelize name} < Spinach::FeatureSteps" 38 | generated_steps = steps.map do |step| 39 | step_generator = Generators::StepGenerator.new(step) 40 | step_generator.generate.split("\n").map do |line| 41 | " #{line}" 42 | end.join("\n") 43 | end 44 | result.puts generated_steps.join("\n\n") 45 | result.puts "end" 46 | result.string 47 | end 48 | 49 | # @return [String] 50 | # an example filename for this feature steps 51 | def filename 52 | Spinach::Support.underscore( 53 | Spinach::Support.camelize name 54 | ) + ".rb" 55 | end 56 | 57 | # @return [String] 58 | # the path where this feature steps may be saved 59 | def path 60 | Spinach.config[:step_definitions_path] 61 | end 62 | 63 | # @return [String] 64 | # the expanded path where this feature steps may be saved 65 | def filename_with_path 66 | File.expand_path File.join(path, filename) 67 | end 68 | 69 | # Stores the example feature steps definition into an expected path 70 | # 71 | def store 72 | if file_exists?(filename) 73 | raise FeatureGeneratorException.new("File #{filename} already exists at #{file_path(filename)}.") 74 | else 75 | FileUtils.mkdir_p path 76 | File.open(filename_with_path, 'w') do |file| 77 | file.write(generate) 78 | puts "Generating #{File.basename(filename_with_path)}" 79 | end 80 | end 81 | end 82 | 83 | private 84 | 85 | def file_exists?(filename) 86 | !!file_path(filename) 87 | end 88 | 89 | def file_path(filename) 90 | Dir.glob("#{path}/**/#{filename}").first 91 | end 92 | end 93 | 94 | class FeatureGeneratorException < Exception; end; 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/spinach/generators/step_generator.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # A step generator generates an example output for a step given the parsed 3 | # feature data. 4 | # 5 | class Generators::StepGenerator 6 | 7 | # @param [Step] step 8 | # The step. 9 | def initialize(step) 10 | @step = step 11 | end 12 | 13 | # @return [String] 14 | # an example step definition 15 | def generate 16 | result = StringIO.new 17 | result.puts "step '#{Spinach::Support.escape_single_commas @step.name}' do" 18 | result.puts " pending 'step not implemented'" 19 | result.puts "end" 20 | result.string 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/spinach/hookable.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # The hookable module includes subscription capabilities to the class in which 3 | # it is included. 4 | # 5 | # Take in account that while most subscription/notification mechanism work 6 | # at the class level, Hookable defines hooks at the instance level - so they 7 | # are not the same in all the class instances. 8 | module Hookable 9 | 10 | def self.included(base) 11 | base.class_eval do 12 | extend ClassMethods 13 | include InstanceMethods 14 | end 15 | end 16 | 17 | module ClassMethods 18 | # Adds a new hook to this class. Every hook defines two methods used to 19 | # add new callbacks and to run them passing a bunch of parameters. 20 | # 21 | # @example 22 | # class 23 | def hook(hook) 24 | define_method hook do |&block| 25 | add_hook(hook, &block) 26 | end 27 | define_method "run_#{hook}" do |*args, &block| 28 | run_hook(hook, *args, &block) 29 | end 30 | end 31 | 32 | # Adds a new around_hook to this class. Every hook defines two methods 33 | # used to add new callbacks and to run them around a given block of code 34 | # passing a bunch of parameters and invoking them in the order they were 35 | # defined. 36 | # 37 | # @example 38 | # class 39 | def around_hook(hook) 40 | define_method hook do |&block| 41 | add_hook(hook, &block) 42 | end 43 | define_method "run_#{hook}" do |*args, &block| 44 | run_around_hook(hook, *args, &block) 45 | end 46 | end 47 | end 48 | 49 | module InstanceMethods 50 | attr_writer :hooks 51 | 52 | # @return [Hash] 53 | # hash in which the key is the hook name and the value an array of any 54 | # defined callbacks, or nil. 55 | def hooks 56 | @hooks ||= {} 57 | end 58 | 59 | # Resets all this class' hooks to a pristine state 60 | def reset 61 | self.hooks = {} 62 | end 63 | 64 | # Runs around hooks in a way that ensure the scenario block is executed 65 | # only once 66 | # 67 | # @param [String] name 68 | # the around hook's name 69 | # 70 | # @param [] args 71 | # the list of arguments to pass to other around filters 72 | # 73 | # @param [Proc] block 74 | # the block containing the scenario action to be executed 75 | def run_around_hook(name, *args, &block) 76 | raise ArgumentError.new("block is mandatory") unless block 77 | if callbacks = hooks[name.to_sym] 78 | callbacks.reverse.inject(block) do |blk, callback| 79 | proc do 80 | callback.call *args do 81 | blk.call 82 | end 83 | end 84 | end.call 85 | else 86 | yield 87 | end 88 | end 89 | 90 | # Runs a particular hook given a set of arguments 91 | # 92 | # @param [String] name 93 | # the hook's name 94 | # 95 | def run_hook(name, *args, &block) 96 | if callbacks = hooks[name.to_sym] 97 | callbacks.each{ |c| c.call(*args, &block) } 98 | else 99 | yield if block 100 | end 101 | end 102 | 103 | # @param [String] name 104 | # the hook's identifier 105 | # 106 | # @return [Array] 107 | # array of hooks for that particular identifier 108 | def hooks_for(name) 109 | hooks[name.to_sym] || [] 110 | end 111 | 112 | # Adds a hook to the queue 113 | # 114 | # @param [String] name 115 | # the hook's identifier 116 | # 117 | # @param [Proc] block 118 | # an action to perform once that hook is executed 119 | def add_hook(name, &block) 120 | hooks[name.to_sym] ||= [] 121 | hooks[name.to_sym] << block 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/spinach/hooks.rb: -------------------------------------------------------------------------------- 1 | require_relative 'hookable' 2 | 3 | module Spinach 4 | # Spinach's hooks is a subscription mechanism to allow developers to define 5 | # certain callbacks given several Spinach signals, like running a feature, 6 | # executing a particular step and such. 7 | class Hooks 8 | include Hookable 9 | 10 | # Runs before the entire spinach run 11 | # 12 | # @example 13 | # Spinach.hooks.before_run do 14 | # # Whatever 15 | # end 16 | hook :before_run 17 | 18 | # Runs after the entire spinach run 19 | # 20 | # @example 21 | # Spinach.hooks.after_run do |status| 22 | # # status is true when the run is successful, false otherwise 23 | # end 24 | hook :after_run 25 | 26 | # Runs before every feature, 27 | # 28 | # @example 29 | # Spinach.hooks.before_feature do |feature_data| 30 | # # feature_data is a hash of the parsed feature data 31 | # end 32 | hook :before_feature 33 | 34 | # Runs after every feature 35 | # 36 | # @example 37 | # Spinach.hooks.after_feature do |feature_data| 38 | # # feature_data is a hash of the parsed feature data 39 | # end 40 | hook :after_feature 41 | 42 | # Runs when an undefined feature is found 43 | # 44 | # @example 45 | # Spinach.hooks.on_undefined_feature do |feature_data, exception| 46 | # # feature_data is a hash of the parsed feature data 47 | # # exception contains the thrown exception 48 | # end 49 | hook :on_undefined_feature 50 | 51 | # Runs before every scenario 52 | # 53 | # @example 54 | # Spinach.hooks.before_scenario do |scenario_data, step_definitions| 55 | # # feature_data is a hash of the parsed scenario data 56 | # end 57 | hook :before_scenario 58 | 59 | # Runs around every scenario 60 | # 61 | # @example 62 | # Spinach.hooks.around_scenario do |scenario_data, step_definitions, &block| 63 | # # feature_data is a hash of the parsed scenario data 64 | # block.call 65 | # end 66 | around_hook :around_scenario 67 | 68 | # Runs after every scenario 69 | # 70 | # @example 71 | # Spinach.hooks.after_scenario do |scenario_data, step_definitions| 72 | # # feature_data is a hash of the parsed scenario data 73 | # end 74 | hook :after_scenario 75 | 76 | # Runs before every step execution 77 | # 78 | # @example 79 | # Spinach.hooks.before_step do |step_data, step_definitions| 80 | # # step_data contains a hash with this step's data 81 | # end 82 | hook :before_step 83 | 84 | # Runs around every step 85 | # 86 | # @example 87 | # Spinach.hooks.around_step do |step_data, step_definitions, &block| 88 | # # step_data contains a hash with this step's data 89 | # block.call 90 | # end 91 | around_hook :around_step 92 | 93 | # Runs after every step execution 94 | # 95 | # @example 96 | # Spinach.hooks.after_step do |step_data, step_definitions| 97 | # # step_data contains a hash with this step's data 98 | # end 99 | hook :after_step 100 | 101 | # Runs after every successful step execution 102 | # 103 | # @example 104 | # Spinach.hooks.on_successful_step do |step_data, location, step_definitions| 105 | # # step_data contains a hash with this step's data 106 | # # step_location contains a string indication this step definition's 107 | # # location 108 | # end 109 | hook :on_successful_step 110 | 111 | # Runs after every failed step execution 112 | # 113 | # @example 114 | # Spinach.hooks.on_failed_step do |step_data, exception, location, step_definitions| 115 | # # step_data contains a hash with this step's data 116 | # # step_location contains a string indication this step definition's 117 | # # location 118 | # end 119 | hook :on_failed_step 120 | 121 | # Runs after every step execution that raises an exception 122 | # 123 | # @example 124 | # Spinach.hooks.on_error_step do |step_data, exception, location, step_definitions| 125 | # # step_data contains a hash with this step's data 126 | # # step_location contains a string indication this step definition's 127 | # # location 128 | # end 129 | hook :on_error_step 130 | 131 | # Runs every time a step which is not defined is called 132 | # 133 | # @example 134 | # Spinach.hooks.on_undefined_step do |step_data, exception, location, step_definitions| 135 | # # step_data contains a hash with this step's data 136 | # # step_location contains a string indication this step definition's 137 | # # location 138 | # end 139 | hook :on_undefined_step 140 | 141 | # Runs every time a pending step is called 142 | # 143 | # @example 144 | # Spinach.hooks.on_pending_step do |step_data, exception| 145 | # # step_data contains a hash with this step's data 146 | # # exception contains the raised exception containing the pending message 147 | # end 148 | hook :on_pending_step 149 | 150 | # Runs every time a step is skipped because there has been an unsuccessful 151 | # one just before. 152 | # 153 | # @example 154 | # Spinach.hooks.on_undefined_step do |step_data, step_definitions| 155 | # # step_data contains a hash with this step's data 156 | # end 157 | hook :on_skipped_step 158 | 159 | # Runs before running a scenario with a particular tag 160 | # 161 | # @param [String] tag 162 | # the tag to match 163 | # 164 | # @example 165 | # Spinach.hooks.on_tag('javascript') do 166 | # # change capybara driver 167 | # end 168 | def on_tag(tag) 169 | before_scenario do |scenario, step_definitions| 170 | tags = scenario.tags 171 | next unless tags.any? 172 | yield(scenario, step_definitions) if tags.include? tag.to_s 173 | end 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/spinach/orderers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'orderers/default' 2 | require_relative 'orderers/random' 3 | -------------------------------------------------------------------------------- /lib/spinach/orderers/default.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | module Orderers 3 | class Default 4 | def initialize(**options); end 5 | 6 | # Appends any necessary report output (by default does nothing). 7 | # 8 | # @param [IO] io 9 | # Output buffer for report. 10 | # 11 | # @api public 12 | def attach_summary(io); end 13 | 14 | # Returns a reordered version of the provided array 15 | # 16 | # @param [Array] items 17 | # Items to order 18 | # 19 | # @api public 20 | def order(items) 21 | items 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/spinach/orderers/random.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | 3 | module Spinach 4 | module Orderers 5 | class Random 6 | attr_reader :seed 7 | 8 | def initialize(seed:) 9 | @seed = seed.to_s 10 | end 11 | 12 | # Output the randomization seed in the report summary. 13 | # 14 | # @param [IO] io 15 | # Output buffer for report. 16 | # 17 | # @api public 18 | def attach_summary(io) 19 | io.puts("Randomized with seed #{seed}\n\n") 20 | end 21 | 22 | # Returns a reordered version of the provided array 23 | # 24 | # @param [Array] items 25 | # Items to order 26 | # 27 | # @api public 28 | def order(items) 29 | items.sort_by do |item| 30 | Digest::MD5.hexdigest(seed + item.ordering_id) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/spinach/parser.rb: -------------------------------------------------------------------------------- 1 | require 'gherkin_ruby' 2 | require_relative 'parser/visitor' 3 | 4 | module Spinach 5 | # Parser leverages GherkinRuby to parse the feature definition. 6 | # 7 | class Parser 8 | # @param [String] content 9 | # The content to parse. 10 | # 11 | # @api public 12 | def initialize(content) 13 | @content = content 14 | end 15 | 16 | # @param [String] filename 17 | # The filename to parse. 18 | # 19 | # @api public 20 | def self.open_file(filename) 21 | new File.read(filename) 22 | end 23 | 24 | # Gets the plain text content out of the feature file. 25 | # 26 | # @return [String] 27 | # The plain feature content. 28 | # 29 | # @api public 30 | attr_reader :content 31 | 32 | # Parses the feature file and returns a Feature. 33 | # 34 | # @return [Feature] 35 | # The Feature. 36 | # 37 | # @api public 38 | def parse 39 | ast = GherkinRuby.parse(@content + "\n") 40 | Visitor.new.visit(ast) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/spinach/parser/visitor.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | class Parser 3 | # The Spinach Visitor traverses the output AST from the GherkinRuby parser and 4 | # populates its Feature with all the scenarios, tags, steps, etc. 5 | # 6 | # @example 7 | # 8 | # ast = GherkinRuby.parse(File.read('some.feature')) 9 | # visitor = Spinach::Parser::Visitor.new 10 | # feature = visitor.visit(ast) 11 | # 12 | class Visitor 13 | attr_reader :feature 14 | 15 | # @param [Feature] feature 16 | # The feature to populate, 17 | # 18 | # @api public 19 | def initialize 20 | @feature = Feature.new 21 | end 22 | 23 | # @param [GherkinRuby::AST::Feature] ast 24 | # The AST root node to visit. 25 | # 26 | # @api public 27 | def visit(ast) 28 | ast.accept(self) 29 | 30 | @feature 31 | end 32 | 33 | # Sets the feature name and iterates over the feature scenarios. 34 | # 35 | # @param [GherkinRuby::AST::Feature] feature 36 | # The feature to visit. 37 | # 38 | # @api public 39 | def visit_Feature(node) 40 | @feature.name = node.name 41 | @feature.description = node.description 42 | 43 | node.background.accept(self) if node.background 44 | 45 | @current_tag_set = @feature 46 | node.tags.each { |tag| tag.accept(self) } 47 | @current_tag_set = nil 48 | 49 | node.scenarios.each { |scenario| scenario.accept(self) } 50 | end 51 | 52 | # Iterates over the steps. 53 | # 54 | # @param [GherkinRuby::AST::Scenario] node 55 | # The scenario to visit. 56 | # 57 | # @api public 58 | def visit_Background(node) 59 | background = Background.new(@feature) 60 | background.line = node.line 61 | 62 | @current_step_set = background 63 | node.steps.each { |step| step.accept(self) } 64 | @current_step_set = nil 65 | 66 | @feature.background = background 67 | end 68 | 69 | # Sets the scenario name and iterates over the steps. 70 | # 71 | # @param [GherkinRuby::AST::Scenario] node 72 | # The scenario to visit. 73 | # 74 | # @api public 75 | def visit_Scenario(node) 76 | scenario = Scenario.new(@feature) 77 | scenario.name = node.name 78 | scenario.lines = [ 79 | node.line, 80 | *node.steps.map(&:line) 81 | ].uniq.sort 82 | 83 | @current_tag_set = scenario 84 | node.tags.each { |tag| tag.accept(self) } 85 | @current_tag_set = nil 86 | 87 | @current_step_set = scenario 88 | node.steps.each { |step| step.accept(self) } 89 | @current_step_set = nil 90 | 91 | @feature.scenarios << scenario 92 | end 93 | 94 | # Adds the tag to the current scenario. 95 | # 96 | # @param [GherkinRuby::AST::Tag] node 97 | # The tag to add. 98 | # 99 | # @api public 100 | def visit_Tag(node) 101 | @current_tag_set.tags << node.name 102 | end 103 | 104 | # Adds the step to the current scenario. 105 | # 106 | # @param [GherkinRuby::AST::Step] step 107 | # The step to add. 108 | # 109 | # @api public 110 | def visit_Step(node) 111 | step = Step.new(@current_step_set) 112 | step.name = node.name 113 | step.line = node.line 114 | step.keyword = node.keyword 115 | 116 | @current_step_set.steps << step 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/spinach/reporter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'colorize' 3 | 4 | module Spinach 5 | # Spinach reporter collects information from Runner hooks and outputs the 6 | # results 7 | # 8 | class Reporter 9 | # Initialize a reporter with an empty error container. 10 | def initialize(options = {}) 11 | @errors = [] 12 | @options = options 13 | @orderer = options[:orderer] 14 | @undefined_features = [] 15 | @successful_steps = [] 16 | @undefined_steps = [] 17 | @failed_steps = [] 18 | @error_steps = [] 19 | @pending_steps = [] 20 | end 21 | 22 | # A Hash with options for the reporter 23 | # 24 | attr_reader :options, :current_feature, :current_scenario 25 | 26 | attr_reader :pending_steps, :undefined_steps, :failed_steps, :error_steps, :undefined_features, :successful_steps 27 | 28 | # Hooks the reporter to the runner endpoints 29 | def bind 30 | Spinach.hooks.tap do |hooks| 31 | hooks.before_run { |*args| before_run(*args) } 32 | hooks.after_run { |*args| after_run(*args) } 33 | hooks.before_feature { |*args| before_feature_run(*args) } 34 | hooks.after_feature { |*args| after_feature_run(*args) } 35 | hooks.on_undefined_feature { |*args| on_feature_not_found(*args) } 36 | hooks.before_scenario { |*args| before_scenario_run(*args) } 37 | hooks.around_scenario { |*args, &block| around_scenario_run(*args, &block) } 38 | hooks.after_scenario { |*args| after_scenario_run(*args) } 39 | hooks.on_successful_step { |*args| on_successful_step(*args) } 40 | hooks.on_undefined_step { |*args| on_undefined_step(*args) } 41 | hooks.on_pending_step { |*args| on_pending_step(*args) } 42 | hooks.on_failed_step { |*args| on_failed_step(*args) } 43 | hooks.on_error_step { |*args| on_error_step(*args) } 44 | hooks.on_skipped_step { |*args| on_skipped_step(*args) } 45 | 46 | hooks.before_feature { |*args| set_current_feature(*args) } 47 | hooks.after_feature { |*args| clear_current_feature(*args) } 48 | hooks.before_scenario { |*args| set_current_scenario(args.first) } 49 | hooks.after_scenario { |*args| clear_current_scenario(args.first) } 50 | end 51 | end 52 | 53 | def before_run(*args); end; 54 | def after_run(*args); end; 55 | def before_feature_run(*args); end 56 | def after_feature_run(*args); end 57 | def on_feature_not_found(*args); end 58 | def before_scenario_run(*args); end 59 | def around_scenario_run(*args) 60 | yield 61 | end 62 | def after_scenario_run(*args); end 63 | def on_successful_step(*args); end; 64 | def on_failed_step(*args); end; 65 | def on_error_step(*args); end; 66 | def on_undefined_step(*args); end; 67 | def on_pending_step(*args); end; 68 | def on_skipped_step(*args); end; 69 | 70 | # Stores the current feature 71 | # 72 | # @param [Feature] 73 | # The feature. 74 | def set_current_feature(feature) 75 | @current_feature = feature 76 | end 77 | 78 | # Clears this current feature 79 | def clear_current_feature(*args) 80 | @current_feature = nil 81 | end 82 | 83 | # Stores the current scenario 84 | # 85 | # @param [Hash] 86 | # the data for this scenario 87 | def set_current_scenario(scenario) 88 | @current_scenario = scenario 89 | end 90 | 91 | # Clears this current scenario 92 | def clear_current_scenario(*args) 93 | @current_scenario = nil 94 | end 95 | end 96 | end 97 | 98 | require_relative 'reporter/stdout' 99 | require_relative 'reporter/progress' 100 | require_relative 'reporter/failure_file' 101 | -------------------------------------------------------------------------------- /lib/spinach/reporter/failure_file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative 'reporting' 3 | require 'pp' 4 | 5 | module Spinach 6 | class Reporter 7 | # The FailureFile reporter outputs failing scenarios to a temporary file, one per line. 8 | # 9 | class FailureFile < Reporter 10 | 11 | # Initializes the output filename and the temporary directory. 12 | # 13 | def initialize(*args) 14 | super(*args) 15 | 16 | # Generate a unique filename for this test run, or use the supplied option 17 | @filename = options[:failure_filename] || ENV['SPINACH_FAILURE_FILE'] || "tmp/spinach-failures_#{Time.now.strftime('%F_%H-%M-%S-%L')}.txt" 18 | 19 | # Create the temporary directory where we will output our file, if necessary 20 | Dir.mkdir('tmp', 0755) unless Dir.exist?('tmp') 21 | 22 | # Collect an array of failing scenarios 23 | @failing_scenarios = [] 24 | end 25 | 26 | attr_reader :failing_scenarios, :filename 27 | 28 | # Writes all failing scenarios to a file, unless our run was successful. 29 | # 30 | # @param [Boolean] success 31 | # Indicates whether the entire test run was successful 32 | # 33 | def after_run(success) 34 | # Save our failed scenarios to a file 35 | File.open(@filename, 'w') { |f| f.write @failing_scenarios.join("\n") } unless success 36 | end 37 | 38 | # Adds a failing step to the output buffer. 39 | # 40 | def on_failed_step(*args) 41 | add_scenario(current_feature.filename, current_scenario.lines[0]) 42 | end 43 | 44 | # Adds a step that has raised an error to the output buffer. 45 | # 46 | def on_error_step(*args) 47 | add_scenario(current_feature.filename, current_scenario.lines[0]) 48 | end 49 | 50 | private 51 | 52 | # Adds a filename and line number to the output buffer, suitable for rerunning. 53 | # 54 | def add_scenario(filename, line) 55 | @failing_scenarios << "#{filename}:#{line}" 56 | end 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /lib/spinach/reporter/progress.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative 'reporting' 3 | 4 | module Spinach 5 | class Reporter 6 | # The Progress reporter outputs the runner results to the standard output 7 | # 8 | class Progress < Reporter 9 | include Reporting 10 | 11 | # Initializes the reporter 12 | # 13 | # @param [Hash] options 14 | # Sets a custom output buffer by setting options[:output] 15 | # Sets a custom error buffer by setting options[:error] 16 | # 17 | def initialize(*args) 18 | super(*args) 19 | @out = options[:output] || $stdout 20 | @error = options[:error] || $stderr 21 | end 22 | 23 | # Adds a passed step to the output buffer. 24 | # 25 | # @param [Step] step 26 | # The step. 27 | # 28 | # @param [Array] step_location 29 | # The step source location 30 | # 31 | def on_successful_step(step, step_location, step_definitions = nil) 32 | output_step('.', :green) 33 | self.scenario = [current_feature, current_scenario, step] 34 | successful_steps << scenario 35 | end 36 | 37 | # Adds a failing step to the output buffer. 38 | # 39 | # @param [Hash] step 40 | # The step in a JSON GherkinRuby format 41 | # 42 | # @param [Exception] failure 43 | # The exception that caused the failure 44 | # 45 | def on_failed_step(step, failure, step_location, step_definitions = nil) 46 | output_step('F', :red) 47 | self.scenario_error = [current_feature, current_scenario, step, failure] 48 | failed_steps << scenario_error 49 | end 50 | 51 | # Adds a step that has raised an error to the output buffer. 52 | # 53 | # @param [Hash] step 54 | # The step in a JSON GherkinRuby format 55 | # 56 | # @param [Exception] failure 57 | # The exception that caused the failure 58 | # 59 | def on_error_step(step, failure, step_location, step_definitions = nil) 60 | output_step('E', :red) 61 | self.scenario_error = [current_feature, current_scenario, step, failure] 62 | error_steps << scenario_error 63 | end 64 | 65 | # Adds an undefined step to the output buffer. 66 | # 67 | # @param [Hash] step 68 | # The step in a JSON GherkinRuby format 69 | # 70 | def on_undefined_step(step, failure, step_definitions = nil) 71 | output_step('U', :yellow) 72 | self.scenario_error = [current_feature, current_scenario, step, failure] 73 | undefined_steps << scenario_error 74 | end 75 | 76 | # Adds an undefined step to the output buffer. 77 | # 78 | # @param [Hash] step 79 | # The step in a JSON GherkinRuby format 80 | # 81 | def on_pending_step(step, failure) 82 | output_step('P', :yellow) 83 | self.scenario_error = [current_feature, current_scenario, step, failure] 84 | pending_steps << scenario_error 85 | end 86 | 87 | # Adds a step that has been skipped to the output buffer. 88 | # 89 | # @param [Hash] step 90 | # The step that GherkinRuby extracts 91 | # 92 | def on_skipped_step(step, step_definitions = nil) 93 | output_step('~', :cyan) 94 | end 95 | 96 | # Adds to the output buffer a step result 97 | # 98 | # @param [String] text 99 | # A symbol to prepend before the step keyword (might be useful to 100 | # indicate if everything went ok or not). 101 | # 102 | # @param [Symbol] color 103 | # The color code to use with Colorize to colorize the output. 104 | # 105 | def output_step(text, color = :grey) 106 | out.print(text.to_s.colorize(color)) 107 | end 108 | end 109 | end 110 | end 111 | 112 | -------------------------------------------------------------------------------- /lib/spinach/reporter/stdout.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative 'reporting' 3 | 4 | module Spinach 5 | class Reporter 6 | # The Stdout reporter outputs the runner results to the standard output 7 | # 8 | class Stdout < Reporter 9 | include Reporting 10 | 11 | # Initializes the reporter 12 | # 13 | # @param [Hash] options 14 | # Sets a custom output buffer by setting options[:output] 15 | # Sets a custom error buffer by setting options[:error] 16 | # 17 | def initialize(*args) 18 | super(*args) 19 | @out = options[:output] || $stdout 20 | @error = options[:error] || $stderr 21 | @max_step_name_length = 0 22 | end 23 | 24 | # Prints the feature name to the standard output 25 | # 26 | # @param [Hash] data 27 | # The feature in a JSON GherkinRubyRuby format 28 | # 29 | def before_feature_run(feature) 30 | name = feature.name 31 | out.puts "\n#{'Feature:'.magenta} #{name.light_magenta}" 32 | end 33 | 34 | # Prints the scenario name to the standard ouput 35 | # 36 | # @param [Hash] data 37 | # The feature in a JSON GherkinRubyRuby format 38 | # 39 | def before_scenario_run(scenario, step_definitions = nil) 40 | @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? 41 | name = scenario.name 42 | out.puts "\n #{'Scenario:'.green} #{name.light_green}" 43 | end 44 | 45 | # Adds an error report and re 46 | # 47 | # @param [Hash] data 48 | # The feature in a JSON GherkinRubyRuby format 49 | # 50 | def after_scenario_run(scenario, step_definitions = nil) 51 | if scenario_error 52 | report_error(scenario_error, :full) 53 | self.scenario_error = nil 54 | end 55 | end 56 | 57 | # Adds a passed step to the output buffer. 58 | # 59 | # @param [Step] step 60 | # The step. 61 | # 62 | # @param [Array] step_location 63 | # The step source location 64 | # 65 | def on_successful_step(step, step_location, step_definitions = nil) 66 | output_step('✔', step, :green, step_location) 67 | self.scenario = [current_feature, current_scenario, step] 68 | successful_steps << scenario 69 | end 70 | 71 | # Adds a failing step to the output buffer. 72 | # 73 | # @param [Hash] step 74 | # The step in a JSON GherkinRubyRuby format 75 | # 76 | # @param [Exception] failure 77 | # The exception that caused the failure 78 | # 79 | def on_failed_step(step, failure, step_location, step_definitions = nil) 80 | output_step('✘', step, :red, step_location) 81 | self.scenario_error = [current_feature, current_scenario, step, failure] 82 | failed_steps << scenario_error 83 | end 84 | 85 | # Adds a step that has raised an error to the output buffer. 86 | # 87 | # @param [Hash] step 88 | # The step in a JSON GherkinRubyRuby format 89 | # 90 | # @param [Exception] failure 91 | # The exception that caused the failure 92 | # 93 | def on_error_step(step, failure, step_location, step_definitions = nil) 94 | output_step('!', step, :red, step_location) 95 | self.scenario_error = [current_feature, current_scenario, step, failure] 96 | error_steps << scenario_error 97 | end 98 | 99 | # Adds an undefined step to the output buffer. 100 | # 101 | # @param [Hash] step 102 | # The step in a JSON GherkinRubyRuby format 103 | # 104 | def on_undefined_step(step, failure, step_definitions = nil) 105 | output_step('?', step, :red) 106 | self.scenario_error = [current_feature, current_scenario, step, failure] 107 | undefined_steps << scenario_error 108 | end 109 | 110 | # Adds an undefined step to the output buffer. 111 | # 112 | # @param [Hash] step 113 | # The step in a JSON GherkinRubyRuby format 114 | # 115 | def on_pending_step(step, failure) 116 | output_step('P', step, :yellow) 117 | self.scenario_error = [current_feature, current_scenario, step, failure] 118 | pending_steps << scenario_error 119 | end 120 | 121 | # Adds a feature not found message to the output buffer. 122 | # 123 | # @param [Hash] feature 124 | # the feature in a json gherkin format 125 | # 126 | # @param [Spinach::FeatureNotFoundException] exception 127 | # the related exception 128 | # 129 | def on_feature_not_found(feature) 130 | generator = Generators::FeatureGenerator.new(feature) 131 | lines = "Could not find steps for `#{feature.name}` feature\n\n" 132 | lines << "\nPlease create the file #{generator.filename} at #{generator.path}, with:\n\n" 133 | 134 | lines << generator.generate 135 | 136 | lines.split("\n").each do |line| 137 | out.puts " #{line}".red 138 | end 139 | out.puts "\n\n" 140 | 141 | undefined_features << feature 142 | end 143 | 144 | # Adds a step that has been skipped to the output buffer. 145 | # 146 | # @param [Hash] step 147 | # The step that GherkinRubyRuby extracts 148 | # 149 | def on_skipped_step(step, step_definitions = nil) 150 | output_step('~', step, :cyan) 151 | end 152 | 153 | # Adds to the output buffer a step result 154 | # 155 | # @param [String] symbol 156 | # A symbol to prepend before the step keyword (might be useful to 157 | # indicate if everything went ok or not). 158 | # 159 | # @param [Hash] step 160 | # The step in a JSON GherkinRubyRuby format 161 | # 162 | # @param [Symbol] color 163 | # The color code to use with Colorize to colorize the output. 164 | # 165 | # @param [Array] step_location 166 | # step source location and file line 167 | # 168 | def output_step(symbol, step, color, step_location = nil) 169 | step_location = step_location.first.gsub("#{File.expand_path('.')}/", '# ')+":#{step_location.last.to_s}" if step_location 170 | max_length = @max_step_name_length + 60 # Colorize and output format correction 171 | 172 | # REMEMBER TO CORRECT PREVIOUS MAX LENGTH IF OUTPUT FORMAT IS MODIFIED 173 | buffer = [] 174 | buffer << indent(4) 175 | buffer << symbol.colorize(:"light_#{color}") 176 | buffer << indent(2) 177 | buffer << step.keyword.colorize(:"light_#{color}") 178 | buffer << indent(1) 179 | buffer << step.name.colorize(color) 180 | joined = buffer.join.ljust(max_length) 181 | 182 | out.puts(joined + step_location.to_s.colorize(:grey)) 183 | end 184 | 185 | private 186 | def indent(n = 1) 187 | " " * n 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/spinach/rspec/mocks.rb: -------------------------------------------------------------------------------- 1 | # Inspired by cucumber/rspec/mocks 2 | require 'rspec/mocks' 3 | 4 | class Spinach::FeatureSteps 5 | include RSpec::Mocks::ExampleMethods 6 | end 7 | 8 | Spinach.hooks.before_scenario do 9 | RSpec::Mocks.setup 10 | end 11 | 12 | Spinach.hooks.after_scenario do 13 | begin 14 | RSpec::Mocks.verify 15 | ensure 16 | RSpec::Mocks.teardown 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/spinach/runner.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # Runner gets the parsed data from the feature and performs the actual calls 3 | # to the feature classes. 4 | # 5 | class Runner 6 | 7 | # The feature files to run 8 | attr_reader :filenames 9 | 10 | # The default path where the steps are located 11 | attr_reader :step_definitions_path 12 | 13 | # The default path where the support files are located 14 | attr_reader :support_path 15 | 16 | # Initializes the runner with a parsed feature 17 | # 18 | # @param [Array] filenames 19 | # A list of feature filenames to run 20 | # 21 | # @param [Hash] options 22 | # 23 | # @option options [String] :step_definitions_path 24 | # The path in which step definitions are found. 25 | # 26 | # @option options [String] :support_path 27 | # The path with the support ruby files. 28 | # 29 | # @api public 30 | def initialize(filenames, options = {}) 31 | @filenames = filenames 32 | 33 | @step_definitions_path = options.delete(:step_definitions_path) || 34 | Spinach.config.step_definitions_path 35 | 36 | @support_path = options.delete(:support_path) || 37 | Spinach.config.support_path 38 | end 39 | 40 | # Inits the reporter with a default one. 41 | # 42 | # @api public 43 | def init_reporters 44 | Spinach.config[:reporter_classes].each do |reporter_class| 45 | reporter_options = default_reporter_options.merge(Spinach.config.reporter_options) 46 | reporter = Support.constantize(reporter_class).new(reporter_options) 47 | 48 | reporter.bind 49 | end 50 | end 51 | 52 | # Runs this runner and outputs the results in a colorful manner. 53 | # 54 | # @return [true, false] 55 | # Whether the run was succesful. 56 | # 57 | # @api public 58 | def run 59 | require_dependencies 60 | require_frameworks 61 | init_reporters 62 | 63 | suite_passed = true 64 | 65 | Spinach.hooks.run_before_run 66 | 67 | features_to_run.each do |feature| 68 | feature_passed = FeatureRunner.new(feature, orderer: orderer).run 69 | suite_passed &&= feature_passed 70 | 71 | break if fail_fast? && !feature_passed 72 | end 73 | 74 | Spinach.hooks.run_after_run(suite_passed) 75 | 76 | suite_passed 77 | end 78 | 79 | # Loads support files and step definitions, ensuring that env.rb is loaded 80 | # first. 81 | # 82 | # @api public 83 | def require_dependencies 84 | required_files.each do |file| 85 | require file 86 | end 87 | end 88 | 89 | # Requires the test framework support 90 | # 91 | def require_frameworks 92 | require_relative 'frameworks' 93 | end 94 | 95 | # Returns an array of files to be required. Sorted by the most nested files first, then alphabetically. 96 | # @return [Array] files 97 | # The step definition files. 98 | # 99 | # @api public 100 | def step_definition_files 101 | Dir.glob( 102 | File.expand_path File.join(step_definitions_path, '**', '*.rb') 103 | ).sort{|a,b| [b.count(File::SEPARATOR), a] <=> [a.count(File::SEPARATOR), b]} 104 | end 105 | 106 | # Returns an array of support files inside the support_path. Will 107 | # put "env.rb" in the beginning 108 | # 109 | # @return [Array] files 110 | # The support files. 111 | # 112 | # @api public 113 | def support_files 114 | support_files = Dir.glob( 115 | File.expand_path File.join(support_path, '**', '*.rb') 116 | ) 117 | environment_file = support_files.find do |f| 118 | f.include?(File.join support_path, 'env.rb') 119 | end 120 | support_files.unshift(environment_file).compact.uniq 121 | end 122 | 123 | # @return [Array] files 124 | # All support files with env.rb ordered first, followed by the step 125 | # definitions. 126 | # 127 | # @api public 128 | def required_files 129 | support_files + step_definition_files 130 | end 131 | 132 | # The orderer for this run. 133 | # 134 | # @api public 135 | def orderer 136 | @orderer ||= Support.constantize(Spinach.config[:orderer_class]).new( 137 | seed: Spinach.config.seed 138 | ) 139 | end 140 | 141 | # Default initialization options for the reporter 142 | # 143 | def default_reporter_options 144 | {orderer: orderer} 145 | end 146 | 147 | private 148 | 149 | def fail_fast? 150 | Spinach.config.fail_fast 151 | end 152 | 153 | def features_to_run 154 | unordered_features = filenames.reduce([]) do |features, filename| 155 | file, *lines = filename.split(":") # little more complex than just a "filename" 156 | 157 | # FIXME Feature should be instantiated directly, not through an unrelated class method 158 | feature = Parser.open_file(file).parse 159 | feature.filename = file 160 | 161 | feature.lines_to_run = lines if lines.any? 162 | 163 | features << feature if TagsMatcher.match_feature(feature) 164 | 165 | features 166 | end 167 | 168 | orderer.order(unordered_features) 169 | end 170 | end 171 | end 172 | 173 | require_relative 'runner/feature_runner' 174 | require_relative 'runner/scenario_runner' 175 | -------------------------------------------------------------------------------- /lib/spinach/runner/feature_runner.rb: -------------------------------------------------------------------------------- 1 | require_relative '../tags_matcher' 2 | require 'spinach/orderers/default' 3 | 4 | module Spinach 5 | class Runner 6 | # A feature runner handles a particular feature run. 7 | # 8 | class FeatureRunner 9 | attr_reader :feature, :orderer 10 | 11 | # @param [GherkinRuby::AST::Feature] feature 12 | # The feature to run. 13 | # 14 | # @api public 15 | def initialize(feature, orderer: Spinach::Orderers::Default.new) 16 | @feature = feature 17 | @orderer = orderer 18 | end 19 | 20 | # @return [String] 21 | # This feature name. 22 | # 23 | # @api public 24 | def feature_name 25 | feature.name 26 | end 27 | 28 | # @return [Array] 29 | # The parsed scenarios for this runner's feature. 30 | # 31 | # @api public 32 | def scenarios 33 | feature.scenarios 34 | end 35 | 36 | # Runs this feature. 37 | # 38 | # @return [true, false] 39 | # Whether the run was successful or not. 40 | # 41 | # @api public 42 | def run 43 | Spinach.hooks.run_before_feature(feature) 44 | 45 | if Spinach.find_step_definitions(feature_name) 46 | run_scenarios! 47 | else 48 | undefined_steps! 49 | end 50 | 51 | Spinach.hooks.run_after_feature(feature) 52 | 53 | # FIXME The feature & scenario runners should have the same structure. 54 | # They should either both return inverted failure or both return 55 | # raw success. 56 | !@failed 57 | end 58 | 59 | private 60 | 61 | def feature_tags 62 | if feature.respond_to?(:tags) 63 | feature.tags 64 | else 65 | [] 66 | end 67 | end 68 | 69 | def run_scenarios! 70 | scenarios_to_run.each do |scenario| 71 | success = ScenarioRunner.new(scenario).run 72 | @failed = true unless success 73 | 74 | break if Spinach.config.fail_fast && @failed 75 | end 76 | end 77 | 78 | def undefined_steps! 79 | Spinach.hooks.run_on_undefined_feature(feature) 80 | 81 | @failed = true 82 | end 83 | 84 | def scenarios_to_run 85 | unordered_scenarios = feature.scenarios.select do |scenario| 86 | has_a_tag_that_will_be_run = TagsMatcher.match(feature_tags + scenario.tags) 87 | on_a_line_that_will_be_run = if feature.run_every_scenario? 88 | true 89 | else 90 | (scenario.lines & feature.lines_to_run).any? 91 | end 92 | 93 | has_a_tag_that_will_be_run && on_a_line_that_will_be_run 94 | end 95 | 96 | orderer.order(unordered_scenarios) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/spinach/runner/scenario_runner.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | class Runner 3 | # A Scenario Runner handles a particular scenario run. 4 | # 5 | class ScenarioRunner 6 | # @param [GherkinRuby::AST::Scenario] scenario 7 | # The scenario. 8 | # 9 | # @api public 10 | def initialize(scenario) 11 | @scenario = scenario 12 | end 13 | 14 | # @return [GherkinRuby::AST::Feature>] 15 | # The feature containing the scenario. 16 | # 17 | # @api public 18 | def feature 19 | @scenario.feature 20 | end 21 | 22 | # @return [Array] 23 | # An array of steps. 24 | # 25 | # @api public 26 | def steps 27 | feature.background_steps + @scenario.steps 28 | end 29 | 30 | # @return [FeatureSteps] 31 | # The step definitions for the current feature. 32 | # 33 | # @api public 34 | def step_definitions 35 | @step_definitions ||= Spinach.find_step_definitions(feature.name).new 36 | end 37 | 38 | # Runs the scenario, capturing any exception, and running the 39 | # corresponding hooks. 40 | # 41 | # @return [true, false] 42 | # Whether the scenario succeeded or not. 43 | # 44 | # @api public 45 | def run 46 | Spinach.hooks.run_before_scenario @scenario, step_definitions 47 | scenario_run = false 48 | Spinach.hooks.run_around_scenario @scenario, step_definitions do 49 | scenario_run = true 50 | step_definitions.before_each 51 | steps.each do |step| 52 | Spinach.hooks.run_before_step step, step_definitions 53 | 54 | if @exception || @has_pending_step 55 | Spinach.hooks.run_on_skipped_step step, step_definitions 56 | else 57 | run_step(step) 58 | end 59 | 60 | Spinach.hooks.run_after_step step, step_definitions 61 | end 62 | step_definitions.after_each 63 | end 64 | raise Spinach::HookNotYieldException.new('around_scenario') if !scenario_run && !@exception 65 | Spinach.hooks.run_after_scenario @scenario, step_definitions 66 | !@exception 67 | end 68 | 69 | # Runs a particular step. 70 | # 71 | # @param [GherkinRuby::AST::Step] step 72 | # The step to be run. 73 | # 74 | # @api semipublic 75 | def run_step(step) 76 | step_location = step_definitions.step_location_for(step.name) 77 | step_run = false 78 | Spinach.hooks.run_around_step step, step_definitions do 79 | step_run = true 80 | step_definitions.execute(step) 81 | end 82 | raise Spinach::HookNotYieldException.new('around_step') if !step_run 83 | Spinach.hooks.run_on_successful_step step, step_location, step_definitions 84 | rescue Spinach::HookNotYieldException => e 85 | raise e 86 | rescue *Spinach.config[:failure_exceptions] => e 87 | @exception = e 88 | Spinach.hooks.run_on_failed_step step, @exception, step_location, step_definitions 89 | rescue Spinach::StepNotDefinedException => e 90 | @exception = e 91 | Spinach.hooks.run_on_undefined_step step, @exception, step_definitions 92 | rescue Spinach::StepPendingException => e 93 | e.step = step 94 | @has_pending_step = true 95 | Spinach.hooks.run_on_pending_step step, e 96 | rescue StandardError => e 97 | @exception = e 98 | Spinach.hooks.run_on_error_step step, @exception, step_location, step_definitions 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/spinach/scenario.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | class Scenario 3 | attr_accessor :lines 4 | attr_accessor :name, :steps, :tags, :feature 5 | 6 | def initialize(feature) 7 | @feature = feature 8 | @steps = [] 9 | @tags = [] 10 | @lines = [] 11 | end 12 | 13 | # Identifier used by orderers. 14 | # 15 | # Needs to involve the relative file path and line number so that the 16 | # ordering a seed generates is stable across both runs and machines. 17 | # 18 | # @api public 19 | def ordering_id 20 | "#{feature.ordering_id}:#{lines.first}" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/spinach/step.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | class Step 3 | attr_accessor :line 4 | attr_accessor :name, :keyword, :scenario 5 | 6 | def initialize(scenario) 7 | @scenario = scenario 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/spinach/support.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # A module to offer helpers for string mangling. 3 | # 4 | module Support 5 | # @param [String] name 6 | # The name to camelize. 7 | # 8 | # @return [String] 9 | # The +name+ in camel case. 10 | # 11 | # @example 12 | # Spinach::Support.camelize('User authentication') 13 | # => 'UserAuthentication' 14 | # 15 | # @api public 16 | def self.camelize(name) 17 | name.to_s.strip.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join 18 | end 19 | 20 | # @param [String] name 21 | # The name to camelize. 22 | # 23 | # @return [String] 24 | # The +name+ in camel case scoped to Spinach::Features. 25 | # 26 | # @example 27 | # Spinach::Support.scoped_camelize('User authentication') 28 | # => 'Spinach::Features::UserAuthentication' 29 | # 30 | # @api public 31 | def self.scoped_camelize(name) 32 | "Spinach::Features::#{camelize(name)}" 33 | end 34 | 35 | # Makes an underscored, lowercase form from the expression in the string. 36 | # 37 | # Changes '::' to '/' to convert namespaces to paths. 38 | # 39 | # Examples: 40 | # "ActiveRecord".underscore # => "active_record" 41 | # "ActiveRecord::Errors".underscore # => active_record/errors 42 | # 43 | # As a rule of thumb you can think of +underscore+ as the inverse of +camelize+, 44 | # though there are cases where that does not hold: 45 | # 46 | # "SSLError".underscore.camelize # => "SslError" 47 | def self.underscore(camel_cased_word) 48 | word = camel_cased_word.to_s.dup 49 | word.gsub!(/::/, '/') 50 | word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') 51 | word.gsub!(/([a-z\\d])([A-Z])/,'\1_\2') 52 | word.tr!("-", "_") 53 | word.tr!(" ", "_") 54 | word.downcase! 55 | word 56 | end 57 | 58 | # Escapes the single commas of a given text. Mostly used in the {Generators} 59 | # classes 60 | # 61 | # @param [String] text 62 | # The text to escape 63 | # 64 | # @return [String] 65 | # The +text+ with escaped commas 66 | # 67 | # @example 68 | # Spinach::Support.escape_single_commas("I've been bad") 69 | # # => "I\'ve been bad" 70 | # 71 | def self.escape_single_commas(text) 72 | text.gsub("'", "\\\\'") 73 | end 74 | 75 | def self.constantize(string) 76 | names = string.split('::') 77 | names.shift if names.empty? || names.first.empty? 78 | 79 | constant = Object 80 | names.each do |name| 81 | constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) 82 | end 83 | constant 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/spinach/tags_matcher.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | module TagsMatcher 3 | 4 | NEGATION_SIGN = '~' 5 | 6 | class << self 7 | 8 | # Matches an array of tags (e.g. of a scenario) against the tags present 9 | # in Spinach's runtime options. 10 | # 11 | # Spinach's tag option is an array which consists of (possibly) multiple 12 | # arrays containing tags provided by the user running the features and 13 | # scenarios. Each of these arrays is considered a tag group. 14 | # 15 | # When matching tags against the tags groups, the tags inside a tag group 16 | # are OR-ed and the tag groups themselves are AND-ed. 17 | def match(tags) 18 | return true if tag_groups.empty? 19 | 20 | tag_groups.all? { |tag_group| 21 | res = match_tag_group(Array(tag_group), tags) 22 | res 23 | } 24 | end 25 | 26 | # Matches the tags of a feature (and its scenarios) against the tags present 27 | # in Spinach's runtime options. 28 | # 29 | # A feature matches when, for any of its scenarios, the combination of the 30 | # feature's tags and that scenario's tags match the configured tags. 31 | def match_feature(feature) 32 | feature.scenarios.any? { |scenario| match(feature.tags + scenario.tags) } 33 | end 34 | 35 | private 36 | 37 | def tag_groups 38 | Spinach.config.tags 39 | end 40 | 41 | def match_tag_group(tag_group, tags) 42 | matched_tags = tag_group.select { |tag| !tag_negated?(tag) } 43 | matched = if matched_tags.empty? 44 | true 45 | else 46 | !tags.empty? && matched_tags.any? { |tag| tags.include?(tag) } 47 | end 48 | 49 | negated_tags = tag_group.select { |tag| tag_negated? tag } 50 | negated = if tags.empty? 51 | false 52 | else 53 | negated_tags.any? {|tag| tags.include?(tag.delete(NEGATION_SIGN))} 54 | end 55 | 56 | !negated && matched 57 | end 58 | 59 | def tag_negated?(tag) 60 | tag.start_with? NEGATION_SIGN 61 | end 62 | 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/spinach/version.rb: -------------------------------------------------------------------------------- 1 | module Spinach 2 | # Spinach version. 3 | VERSION = "0.12.0" 4 | end 5 | -------------------------------------------------------------------------------- /spinach.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/spinach/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Josep Jaume Rey", "Josep M. Bach", "Oriol Gual", "Marc Divins Castellvi"] 6 | gem.email = ["josep.m.bach@gmail.com", "oriolgual@gmail.com", "josepjaume@gmail.com", "marcdivc@gmail.com"] 7 | gem.description = %q{Spinach is a BDD framework on top of gherkin} 8 | gem.summary = %q{Spinach is a BDD framework on top of gherkin} 9 | gem.homepage = "http://github.com/codegram/spinach" 10 | gem.license = 'MIT' 11 | 12 | gem.add_runtime_dependency 'gherkin-ruby', '>= 0.3.2' 13 | gem.add_runtime_dependency 'colorize' 14 | gem.add_development_dependency 'rake' 15 | gem.add_development_dependency 'mocha', "~> 1.5.0" 16 | gem.add_development_dependency 'sinatra' 17 | gem.add_development_dependency 'capybara' 18 | gem.add_development_dependency 'pry' 19 | gem.add_development_dependency 'rspec' 20 | gem.add_development_dependency 'minitest', '< 5.0' 21 | gem.add_development_dependency 'fakefs', ">= 0.5.2" 22 | 23 | gem.required_ruby_version = Gem::Requirement.new(">= 2.4".freeze) 24 | 25 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 26 | gem.files = `git ls-files`.split("\n") 27 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 28 | gem.name = "spinach" 29 | gem.require_paths = ["lib"] 30 | gem.version = Spinach::VERSION 31 | end 32 | -------------------------------------------------------------------------------- /test/spinach/background_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Spinach 4 | describe Background do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/spinach/capybara_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'capybara' 3 | require 'rack/test' 4 | require 'spinach' 5 | require 'spinach/capybara' 6 | require 'sinatra/base' 7 | 8 | describe Spinach::FeatureSteps::Capybara do 9 | let(:sinatra_class) do 10 | Class.new(Sinatra::Base) do 11 | get '/' do 12 | 'Hello world!' 13 | end 14 | end 15 | end 16 | 17 | before do 18 | Capybara.current_driver = :rack_test 19 | Capybara.app = sinatra_class 20 | 21 | class TestFeature < Spinach::FeatureSteps 22 | include Spinach::FeatureSteps::Capybara 23 | feature 'A test feature' 24 | Given 'Hello' do 25 | end 26 | Then 'Goodbye' do 27 | end 28 | Given 'Fail' do 29 | raise StandardError 30 | end 31 | def go_home 32 | visit '/' 33 | page 34 | end 35 | end 36 | 37 | @feature_class = TestFeature 38 | @feature = @feature_class.new 39 | end 40 | 41 | let(:parsed_feature) { Spinach::Parser.new(""" 42 | Feature: A test feature 43 | Scenario: A test scenario 44 | Given Hello 45 | Then Goodbye 46 | 47 | @javascript 48 | Scenario: Another test scenario 49 | Given Hello 50 | Then Goodbye 51 | """).parse 52 | } 53 | 54 | let(:failing_feature) { Spinach::Parser.new(""" 55 | Feature: A test feature 56 | Scenario: A test scenario 57 | Given Fail 58 | """).parse 59 | } 60 | 61 | it 'responds to Capybara::DSL methods' do 62 | @feature.respond_to?(:page).must_equal true 63 | end 64 | 65 | it 'does not respond to non Capybara::DSL methods' do 66 | @feature.respond_to?(:strange).must_equal false 67 | end 68 | 69 | it 'raises NoMethodError when calling a non Capybara::DSL methods' do 70 | proc { @feature.strange }.must_raise(NoMethodError) 71 | end 72 | 73 | it 'goes to a capybara page and returns its result' do 74 | page = @feature.go_home 75 | page.has_content?('Hello world').must_equal true 76 | end 77 | 78 | it 'resets the capybara session after each scenario' do 79 | @feature_runner = Spinach::Runner::FeatureRunner.new(parsed_feature) 80 | 81 | Capybara.current_session.expects(:reset!).at_least_once 82 | 83 | @feature_runner.run 84 | end 85 | 86 | it 'resets the javascript driver after each scenario' do 87 | @feature_runner = Spinach::Runner::FeatureRunner.new(parsed_feature) 88 | 89 | Capybara.expects(:use_default_driver).at_least(2) 90 | 91 | @feature_runner.run 92 | end 93 | 94 | it 'changes the javascript driver when an scenario has the @javascript tag' do 95 | @feature_runner = Spinach::Runner::FeatureRunner.new(parsed_feature) 96 | 97 | Capybara.expects(:javascript_driver).at_least(1) 98 | Capybara.expects(:current_driver=).at_least(1) 99 | 100 | @feature_runner.run 101 | end 102 | 103 | describe "when there's a failure" do 104 | it 'saves and open the page if the option is activated' do 105 | @feature_runner = Spinach::Runner::FeatureRunner.new(failing_feature) 106 | Spinach.config.save_and_open_page_on_failure = true 107 | @feature_class.any_instance.expects(:save_and_open_page).once 108 | @feature_runner.run 109 | Spinach.config.save_and_open_page_on_failure = false 110 | end 111 | 112 | it "doesn't saves and open the page if the option is deactivated" do 113 | @feature_runner = Spinach::Runner::FeatureRunner.new(failing_feature) 114 | Spinach.config.save_and_open_page_on_failure = false 115 | Capybara.expects(:save_and_open_page).never 116 | @feature_runner.run 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/spinach/config_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::Config do 4 | subject do 5 | Spinach::Config.new 6 | end 7 | 8 | describe '#features_path' do 9 | it 'returns a default' do 10 | subject[:features_path].must_be_kind_of String 11 | end 12 | 13 | it 'can be overwritten' do 14 | subject[:features_path] = 'test' 15 | subject[:features_path].must_equal 'test' 16 | end 17 | end 18 | 19 | describe '#reporter_classes' do 20 | it 'returns a default' do 21 | subject[:reporter_classes].must_equal ["Spinach::Reporter::Stdout"] 22 | end 23 | 24 | it 'can be overwritten' do 25 | subject[:reporter_classes] = ["MyOwnReporter"] 26 | subject[:reporter_classes].must_equal ["MyOwnReporter"] 27 | end 28 | end 29 | 30 | describe '#orderer_class' do 31 | it 'returns a default' do 32 | subject[:orderer_class].must_equal "Spinach::Orderers::Default" 33 | end 34 | 35 | it 'can be overwritten' do 36 | subject[:orderer_class] = "MyOwnOrderer" 37 | subject[:orderer_class].must_equal "MyOwnOrderer" 38 | end 39 | end 40 | 41 | describe '#seed' do 42 | it 'has a default' do 43 | subject[:seed].must_be_kind_of Integer 44 | end 45 | 46 | it 'can be overwritten' do 47 | subject[:seed] = 54321 48 | subject[:seed].must_equal 54321 49 | end 50 | end 51 | 52 | describe '#step_definitions_path' do 53 | it 'returns a default' do 54 | subject[:step_definitions_path].must_be_kind_of String 55 | end 56 | 57 | it 'can be overwritten' do 58 | subject[:step_definitions_path] = 'steps' 59 | subject[:step_definitions_path].must_equal 'steps' 60 | end 61 | end 62 | 63 | describe '#support_path' do 64 | it 'returns a default' do 65 | subject[:support_path].must_be_kind_of String 66 | end 67 | 68 | it 'can be overwritten' do 69 | subject[:support_path] = 'support' 70 | subject[:support_path].must_equal 'support' 71 | end 72 | end 73 | 74 | describe '#failure_exceptions' do 75 | it 'returns a default' do 76 | subject[:failure_exceptions].must_be_kind_of Array 77 | end 78 | 79 | it 'can be overwritten' do 80 | subject[:failure_exceptions] = [1, 2, 3] 81 | subject[:failure_exceptions].must_equal [1,2,3] 82 | end 83 | 84 | it 'allows adding elements' do 85 | subject[:failure_exceptions] << RuntimeError 86 | subject[:failure_exceptions].must_include RuntimeError 87 | end 88 | end 89 | 90 | describe '#config_path' do 91 | it 'returns a default' do 92 | subject[:config_path].must_equal 'config/spinach.yml' 93 | end 94 | 95 | it 'can be overwritten' do 96 | subject[:config_path] = 'my_config_file.yml' 97 | subject[:config_path].must_equal 'my_config_file.yml' 98 | end 99 | end 100 | 101 | describe '#parse_from_file' do 102 | before do 103 | subject[:config_path] = 'a_path' 104 | YAML.stubs(:load_file).returns({support_path: 'my_path', config_path: 'another_path'}) 105 | subject.parse_from_file 106 | end 107 | 108 | it 'sets the options' do 109 | subject[:support_path].must_equal 'my_path' 110 | end 111 | 112 | it "doesn't set the config_path option" do 113 | subject[:config_path].must_equal 'a_path' 114 | end 115 | end 116 | 117 | describe '#tags' do 118 | it 'returns a default' do 119 | subject[:tags].must_be_kind_of Array 120 | end 121 | 122 | it 'can be overwritten' do 123 | subject[:tags] = ['wip'] 124 | subject[:tags].must_equal ['wip'] 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/spinach/dsl_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::DSL do 4 | before do 5 | @feature = Class.new do 6 | include Spinach::DSL 7 | end 8 | end 9 | 10 | describe 'class methods' do 11 | describe '#step' do 12 | it 'defines a method with the step name' do 13 | step_executed = false 14 | @feature.step('I say goodbye') do 15 | step_executed = true 16 | end 17 | 18 | @feature.new.execute(stub(name: 'I say goodbye')) 19 | step_executed.must_equal true 20 | end 21 | end 22 | 23 | describe '#Give, #When, #Then, #And, #But' do 24 | it 'are #step aliases' do 25 | %w{Given When Then And But}.each do |method| 26 | @feature.must_respond_to method 27 | end 28 | end 29 | end 30 | 31 | describe "#after" do 32 | let(:super_class) { 33 | Class.new do 34 | attr_reader :var1 35 | def after_each 36 | @var1 = 30 37 | @var2 = 60 38 | end 39 | end 40 | } 41 | 42 | let(:feature) { 43 | Class.new(super_class) do 44 | attr_accessor :var2 45 | include Spinach::DSL 46 | after do 47 | self.var2 = 40 48 | end 49 | end 50 | } 51 | 52 | let(:object) { feature.new } 53 | 54 | it "defines after_each method and calls the super first" do 55 | object.after_each 56 | object.var1.must_equal 30 57 | end 58 | 59 | it "defines after_each method and runs the block second" do 60 | object.after_each 61 | object.var2.must_equal 40 62 | end 63 | 64 | describe "deep inheritance" do 65 | let(:sub_feature) { 66 | Class.new(feature) do 67 | include Spinach::DSL 68 | attr_reader :var3 69 | 70 | after do 71 | @var3 = 15 72 | end 73 | end 74 | } 75 | 76 | let(:sub_object) { sub_feature.new } 77 | 78 | it "works with the 3rd layer of inheritance" do 79 | sub_object.after_each 80 | sub_object.var2.must_equal 40 81 | end 82 | end 83 | end 84 | 85 | describe "#before" do 86 | let(:super_class) { 87 | Class.new do 88 | attr_reader :var1 89 | def before_each 90 | @var1 = 30 91 | @var2 = 60 92 | end 93 | end 94 | } 95 | 96 | let(:feature) { 97 | Class.new(super_class) do 98 | attr_accessor :var2 99 | include Spinach::DSL 100 | before do 101 | self.var2 = 40 102 | end 103 | end 104 | } 105 | 106 | let(:object) { feature.new } 107 | 108 | it "defines before_each method and calls the super first" do 109 | object.before_each 110 | object.var1.must_equal 30 111 | end 112 | 113 | it "defines before_each method and runs the block second" do 114 | object.before_each 115 | object.var2.must_equal 40 116 | end 117 | 118 | describe "deep inheritance" do 119 | let(:sub_feature) { 120 | Class.new(feature) do 121 | include Spinach::DSL 122 | attr_reader :var3 123 | 124 | before do 125 | @var3 = 15 126 | end 127 | end 128 | } 129 | 130 | let(:sub_object) { sub_feature.new } 131 | 132 | it "works with the 3rd layer of inheritance" do 133 | sub_object.before_each 134 | sub_object.var2.must_equal 40 135 | end 136 | end 137 | end 138 | 139 | describe '#feature' do 140 | it 'sets the name for this feature' do 141 | @feature.feature('User salutes') 142 | @feature.feature_name.must_equal 'User salutes' 143 | end 144 | end 145 | 146 | describe "#name" do 147 | it "responds with a feature's name" do 148 | @feature.feature("A cool feature") 149 | @feature.new.name.must_equal "A cool feature" 150 | end 151 | end 152 | 153 | describe '#step_location_for' do 154 | it 'returns step source location' do 155 | @feature.When('I say goodbye') do 156 | 'You say hello' 157 | end 158 | 159 | @feature.new.step_location_for('I say goodbye').first.must_include '/dsl_test.rb' 160 | @feature.new.step_location_for('I say goodbye').last.must_be_kind_of Integer 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/spinach/feature_steps_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::FeatureSteps do 4 | describe 'ancestors' do 5 | it 'is extended by the DSL' do 6 | Spinach::FeatureSteps.ancestors.must_include Spinach::DSL 7 | end 8 | end 9 | 10 | describe 'class methods' do 11 | describe '#inherited' do 12 | it 'registers any feature subclass' do 13 | @feature_steps1 = Class.new(Spinach::FeatureSteps) 14 | @feature_steps2 = Class.new(Spinach::FeatureSteps) 15 | @feature_steps3 = Class.new 16 | 17 | Spinach.feature_steps.must_include @feature_steps1 18 | Spinach.feature_steps.must_include @feature_steps2 19 | Spinach.feature_steps.wont_include @feature_steps3 20 | end 21 | end 22 | end 23 | 24 | describe 'instance methods' do 25 | let(:feature) do 26 | Class.new(Spinach::FeatureSteps) do 27 | When 'I go to the toilet' do 28 | @pee = true 29 | end 30 | attr_reader :pee 31 | end.new 32 | end 33 | 34 | it "responds to before_each" do 35 | feature.must_respond_to(:before_each) 36 | end 37 | 38 | it "responds to after_each" do 39 | feature.must_respond_to(:after_each) 40 | end 41 | 42 | describe 'execute' do 43 | it 'runs defined step correctly' do 44 | feature.execute(stub(name: 'I go to the toilet')) 45 | 46 | feature.pee.must_equal true 47 | end 48 | 49 | it 'raises an exception if step is not defined' do 50 | proc { 51 | feature.execute(stub(name: 'I am lost')) 52 | }.must_raise Spinach::StepNotDefinedException 53 | end 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/spinach/feature_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Spinach 4 | describe Feature do 5 | describe "#lines_to_run=" do 6 | subject { Feature.new } 7 | 8 | before { subject.lines_to_run = [4, 12] } 9 | 10 | it 'writes lines_to_run' do 11 | subject.lines_to_run.must_equal [4, 12] 12 | end 13 | end 14 | 15 | describe '#run_every_scenario?' do 16 | subject { Feature.new } 17 | 18 | describe 'when no line constraints have been specified' do 19 | it 'is true' do 20 | subject.run_every_scenario?.must_equal true 21 | end 22 | end 23 | 24 | describe 'when line constraints have been specified' do 25 | before { subject.lines_to_run = [4, 12] } 26 | 27 | it 'is false' do 28 | subject.run_every_scenario?.must_equal false 29 | end 30 | end 31 | end 32 | 33 | describe "#ordering_id" do 34 | subject { Feature.new } 35 | 36 | before { subject.filename = "features/foo/bar.feature" } 37 | 38 | it 'is the filename' do 39 | subject.ordering_id.must_equal "features/foo/bar.feature" 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/spinach/frameworks/minitest_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | describe "minitest framework" do 4 | before do 5 | require_relative '../../../lib/spinach/frameworks/minitest' 6 | end 7 | 8 | it "adds MiniTest::Assertion into the failure exceptions" do 9 | Spinach.config[:failure_exceptions].must_include MiniTest::Assertion 10 | end 11 | 12 | it "extends the FeatureSteps class with MiniTest DSL" do 13 | Spinach::FeatureSteps.ancestors.must_include MiniTest::Assertions 14 | end 15 | 16 | it "makes FeatureSteps respond to 'assertions'" do 17 | Spinach::FeatureSteps.new.must_respond_to :assertions 18 | end 19 | 20 | it "initializes FeatureSteps' assertions with zero" do 21 | Spinach::FeatureSteps.new.assertions.must_equal 0 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/spinach/generators/feature_generator_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | require_relative '../../../lib/spinach/generators' 3 | 4 | module Spinach::Generators 5 | describe FeatureGenerator do 6 | subject do 7 | FeatureGenerator.new(feature) 8 | end 9 | 10 | let(:feature) do 11 | Spinach::Parser.new(""" 12 | Feature: Cheezburger can I has 13 | Background: 14 | Given I liek cheezburger 15 | Scenario: Some Lulz 16 | Given I haz a sad 17 | When I get some lulz 18 | Then I haz a happy 19 | Scenario: Some sad 20 | Given I haz a happy 21 | When I get some lulz 22 | Then I am OMG ROFLMAO""").parse 23 | end 24 | 25 | describe "#name" do 26 | it "returns the feature name" do 27 | subject.name.must_equal 'Cheezburger can I has' 28 | end 29 | end 30 | 31 | describe "#steps" do 32 | it "returns a correct number different steps for this data" do 33 | subject.steps.length.must_equal 5 34 | end 35 | end 36 | 37 | describe "#generate" do 38 | it "generates an entire feature_steps class definition" do 39 | result = subject.generate 40 | result.must_match(/step 'I haz a sad' do/) 41 | result.must_match(/pending 'step not implemented'/) 42 | end 43 | 44 | it 'scopes the generated class to prevent conflicts' do 45 | result = subject.generate 46 | result.must_match(/class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps/) 47 | end 48 | end 49 | 50 | describe "#filename" do 51 | it "returns a valid filename for the feature" do 52 | subject.filename.must_equal "cheezburger_can_i_has.rb" 53 | end 54 | end 55 | 56 | describe "#path" do 57 | it "should return a valid path" do 58 | subject.path.must_include 'features/steps' 59 | end 60 | end 61 | 62 | describe "#filename_with_path" do 63 | it "should the filename prepended with the path" do 64 | subject.filename_with_path. 65 | must_include 'features/steps/cheezburger_can_i_has.rb' 66 | end 67 | end 68 | 69 | describe "#store" do 70 | it "stores the generated feature into a file" do 71 | in_current_dir do 72 | subject.store 73 | File.directory?("features/steps/").must_equal true 74 | File.exist?("features/steps/cheezburger_can_i_has.rb").must_equal true 75 | File.read("features/steps/cheezburger_can_i_has.rb").strip.must_equal( 76 | subject.generate.strip 77 | ) 78 | FileUtils.rm_rf("features/steps") 79 | end 80 | end 81 | 82 | it "raises an error if the file already exists and does nothing" do 83 | file = "features/steps/cheezburger_can_i_has.rb" 84 | in_current_dir do 85 | FileUtils.mkdir_p "features/steps" 86 | 87 | File.open(file, 'w') do |f| 88 | f.write("Fake content") 89 | end 90 | 91 | Proc.new{subject.store}.must_raise( 92 | Spinach::Generators::FeatureGeneratorException) 93 | 94 | FileUtils.rm_rf("features/steps") 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/spinach/generators/step_generator_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | require_relative '../../../lib/spinach/generators' 3 | 4 | module Spinach::Generators 5 | describe StepGenerator do 6 | subject do 7 | StepGenerator.new(step) 8 | end 9 | 10 | let(:step) do 11 | stub(keyword: 'Given', name: 'I has a sad') 12 | end 13 | 14 | describe "#generate" do 15 | it "generates a step" do 16 | subject.generate.must_match /step.*I has a sad/ 17 | end 18 | 19 | it "generates a pending step" do 20 | subject.generate.must_include "pending 'step not implemented'" 21 | end 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/spinach/generators_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::Generators do 4 | subject do 5 | Spinach::Generators 6 | end 7 | 8 | describe "#generate_feature" do 9 | it "outputs a message if feature cannot be generated" do 10 | in_current_dir do 11 | FileUtils.mkdir_p "features/steps" 12 | 13 | File.open('features/steps/cheezburger_can_i_has.rb', 'w') do |f| 14 | f.write("Feature: Fake feature") 15 | end 16 | 17 | Spinach::Generators::FeatureGenerator.any_instance.expects(:store).raises( 18 | Spinach::Generators::FeatureGeneratorException.new("File already exists")) 19 | 20 | capture_stdout do 21 | subject.run(["features/steps/cheezburger_can_i_has.rb"]) 22 | end.must_include "File already exists" 23 | 24 | FileUtils.rm_rf("features/steps") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/spinach/hookable_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::Hookable do 4 | subject do 5 | Class.new do 6 | include Spinach::Hookable 7 | end.new 8 | end 9 | 10 | describe ".define_hook" do 11 | it "defines a new hook" do 12 | subject.class.hook :before_save 13 | subject.must_respond_to :before_save 14 | end 15 | 16 | it "defines a new around hook" do 17 | subject.class.around_hook :around_save 18 | subject.must_respond_to :around_save 19 | end 20 | end 21 | 22 | describe "hooking mechanism" do 23 | describe "without params" do 24 | it "adds a new hook to the queue" do 25 | subject.add_hook(:before_save) do 26 | end 27 | (subject.hooks_for(:before_save).empty?).must_equal false 28 | end 29 | 30 | it "allows to run a hook" do 31 | arbitrary_variable = false 32 | subject.add_hook(:before_save) do 33 | arbitrary_variable = true 34 | end 35 | subject.run_hook(:before_save) 36 | arbitrary_variable.must_equal true 37 | end 38 | 39 | it "allows to run around hook" do 40 | arbitrary_variable = false 41 | subject.add_hook(:around_save) do |&block| 42 | arbitrary_variable = true 43 | block.call 44 | end 45 | subject.run_around_hook(:around_save) do 46 | end 47 | arbitrary_variable.must_equal true 48 | end 49 | end 50 | 51 | describe "with params" do 52 | it "adds a new hook to the queue" do 53 | subject.add_hook(:before_save) do |var1, var2| 54 | end 55 | (subject.hooks_for(:before_save).empty?).must_equal false 56 | end 57 | 58 | it "allows to run a hook" do 59 | array = [] 60 | subject.add_hook(:before_save) do |var1, var2| 61 | array << var1 62 | array << var2 63 | end 64 | subject.run_hook(:before_save, 1, 2) 65 | array.must_equal [1, 2] 66 | end 67 | 68 | it "allows to run an around hook" do 69 | array = [] 70 | subject.add_hook(:around_save) do |var1, var2, &block| 71 | array << var1 72 | array << var2 73 | block.call 74 | end 75 | subject.run_around_hook(:around_save, 1, 2) do 76 | end 77 | array.must_equal [1, 2] 78 | end 79 | 80 | it "yields to hook block even if nothing is hooked" do 81 | called = false 82 | subject.run_hook(:before_save) do 83 | called = true 84 | end 85 | called.must_equal true 86 | end 87 | 88 | it "yields to around hook block even if nothing is hooked" do 89 | called = false 90 | subject.run_hook(:around_save) do 91 | called = true 92 | end 93 | called.must_equal true 94 | end 95 | end 96 | 97 | describe "order" do 98 | it "runs hooks in registration order" do 99 | save = sequence("save") 100 | object = mock("object") 101 | object.expects(:before_first).in_sequence(save) 102 | object.expects(:before_second).in_sequence(save) 103 | 104 | subject.add_hook(:before_save) do 105 | object.before_first 106 | end 107 | subject.add_hook(:before_save) do 108 | object.before_second 109 | end 110 | subject.run_hook(:before_save) 111 | end 112 | 113 | it "runs around hooks in registration order" do 114 | save = sequence("save") 115 | object = mock("object") 116 | object.expects(:before_first).in_sequence(save) 117 | object.expects(:before_second).in_sequence(save) 118 | object.expects(:after_second).in_sequence(save) 119 | object.expects(:after_first).in_sequence(save) 120 | 121 | subject.add_hook(:around_save) do |&block| 122 | object.before_first 123 | block.call 124 | object.after_first 125 | end 126 | subject.add_hook(:around_save) do |&block| 127 | object.before_second 128 | block.call 129 | object.after_second 130 | end 131 | 132 | subject.run_around_hook(:around_save) {} 133 | end 134 | end 135 | 136 | it "requires a block when running around hook" do 137 | subject.add_hook(:around_save) do 138 | end 139 | lambda { 140 | subject.run_around_hook(:around_save) 141 | }.must_raise ArgumentError 142 | end 143 | 144 | it "runs around hook block only once" do 145 | object = mock("object") 146 | object.expects(:save).once 147 | subject.add_hook(:around_save){|&block| block.call} 148 | subject.add_hook(:around_save){|&block| block.call} 149 | subject.run_around_hook(:around_save){ object.save } 150 | end 151 | 152 | describe "#reset_hooks" do 153 | it "resets the hooks to a pristine state" do 154 | subject.add_hook(:before_save) 155 | (subject.hooks.empty?).must_equal false 156 | subject.reset 157 | (subject.hooks.empty?).must_equal true 158 | end 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /test/spinach/hooks_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::Hooks do 4 | subject do 5 | Spinach::Hooks.new 6 | end 7 | 8 | describe "hooks" do 9 | %w{ 10 | before_run 11 | after_run 12 | before_feature 13 | after_feature 14 | on_undefined_feature 15 | before_scenario 16 | after_scenario 17 | before_step 18 | after_step 19 | on_successful_step 20 | on_failed_step 21 | on_error_step 22 | on_undefined_step 23 | on_skipped_step 24 | on_pending_step 25 | }.each do |callback| 26 | describe "#{callback}" do 27 | it "responds to #{callback}" do 28 | subject.must_respond_to callback 29 | end 30 | 31 | it "executes the hook with params" do 32 | array = [] 33 | block = Proc.new do |arg1, arg2| 34 | array << arg1 35 | array << arg2 36 | end 37 | subject.send(callback, &block) 38 | subject.send("run_#{callback}", 1, 2) 39 | array.must_equal [1, 2] 40 | end 41 | end 42 | end 43 | 44 | describe "around_scenario" do 45 | it "responds to around_scenario" do 46 | subject.must_respond_to :around_scenario 47 | end 48 | 49 | it "executes the hook with params" do 50 | array = [] 51 | block = Proc.new do |arg1, arg2| 52 | array << arg1 53 | array << arg2 54 | end 55 | subject.send(:around_scenario, &block) 56 | subject.send("run_around_scenario", 1, 2) do 57 | end 58 | array.must_equal [1, 2] 59 | end 60 | end 61 | 62 | describe "around_step" do 63 | it "responds to around_step" do 64 | subject.must_respond_to :around_step 65 | end 66 | 67 | it "executes the hook with params" do 68 | array = [] 69 | block = Proc.new do |arg1, arg2| 70 | array << arg1 71 | array << arg2 72 | end 73 | subject.send(:around_step, &block) 74 | subject.send("run_around_step", 1, 2) do 75 | end 76 | array.must_equal [1, 2] 77 | end 78 | end 79 | 80 | describe "#on_tag" do 81 | let(:scenario) do 82 | stub(tags: ['javascript', 'capture']) 83 | end 84 | let(:step_definitions) do 85 | stub(something: "step_definitions") 86 | end 87 | 88 | it "calls the block if the scenario includes the tag" do 89 | assertion = false 90 | subject.on_tag('javascript') do 91 | assertion = true 92 | end 93 | subject.run_before_scenario(scenario, step_definitions) 94 | assertion.must_equal true 95 | end 96 | 97 | it "passes in the step_definitions" do 98 | assertion = false 99 | subject.on_tag('javascript') do |scenario, step_definitions| 100 | assertion = step_definitions.something 101 | end 102 | subject.run_before_scenario(scenario, step_definitions) 103 | assertion.must_equal "step_definitions" 104 | end 105 | 106 | it "doesn't call the block if the scenario doesn't include the tag" do 107 | assertion = false 108 | subject.on_tag('screenshot') do 109 | assertion = true 110 | end 111 | subject.run_before_scenario(scenario, step_definitions) 112 | assertion.wont_equal true 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/spinach/orderers/default_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | describe Spinach::Orderers::Default do 4 | let(:orderer) { Spinach::Orderers::Default.new } 5 | 6 | describe "#attach_summary" do 7 | let(:io) { StringIO.new } 8 | 9 | it 'appends nothing' do 10 | contents_before_running = io.string.dup 11 | 12 | orderer.attach_summary(io) 13 | 14 | io.string.must_equal contents_before_running 15 | end 16 | end 17 | 18 | describe "#order" do 19 | let(:items) { Array(1..10) } 20 | 21 | it "doesn't change the order of the items" do 22 | orderer.order(items).must_equal items 23 | end 24 | end 25 | 26 | describe "#initialize" do 27 | it "can be provided options without raising an error" do 28 | Spinach::Orderers::Default.new(seed: "seed") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/spinach/orderers/random_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | describe Spinach::Orderers::Random do 4 | let(:orderer) { Spinach::Orderers::Random.new(seed: Spinach.config.seed) } 5 | 6 | describe "#attach_summary" do 7 | let(:io) { StringIO.new } 8 | 9 | it 'appends the seed' do 10 | orderer.attach_summary(io) 11 | 12 | io.string.must_match /Randomized\ with\ seed\ #{orderer.seed}/ 13 | end 14 | end 15 | 16 | describe "#order" do 17 | Identifiable = Struct.new(:ordering_id) 18 | 19 | let(:items) { (1..10).map { |n| Identifiable.new(n.to_s) } } 20 | 21 | it "randomizes the items" do 22 | orderer.order(items).wont_equal items 23 | end 24 | 25 | it "always randomizes items the same way with the same seed" do 26 | orderer.order(items).must_equal orderer.order(items) 27 | end 28 | end 29 | 30 | describe "#initialize" do 31 | it "requires a seed parameter" do 32 | proc { 33 | Spinach::Orderers::Random.new 34 | }.must_raise ArgumentError 35 | 36 | Spinach::Orderers::Random.new(seed: 4) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/spinach/parser/visitor_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | module Spinach 4 | class Parser 5 | describe Visitor do 6 | let(:feature) { Feature.new } 7 | let(:visitor) { Visitor.new } 8 | 9 | describe '#visit' do 10 | it 'makes ast accept self' do 11 | ast = stub('AST') 12 | ast.expects(:accept).with(visitor) 13 | 14 | visitor.visit(ast) 15 | end 16 | 17 | it 'returns the feature' do 18 | ast = stub_everything 19 | visitor.instance_variable_set(:@feature, feature) 20 | visitor.visit(ast).must_equal feature 21 | end 22 | end 23 | 24 | describe '#visit_Feature' do 25 | before do 26 | @background = stub_everything 27 | @tags = [stub_everything, stub_everything, stub_everything] 28 | @scenarios = [stub_everything, stub_everything, stub_everything] 29 | @node = stub( 30 | scenarios: @scenarios, 31 | name: 'Go shopping', 32 | description: ['some non-interpreted info','from the description'], 33 | background: @background, 34 | tags: @tags 35 | ) 36 | end 37 | 38 | it 'sets the name' do 39 | visitor.visit_Feature(@node) 40 | visitor.feature.name.must_equal 'Go shopping' 41 | end 42 | 43 | it 'sets the description' do 44 | visitor.visit_Feature(@node) 45 | visitor.feature.description.must_equal ['some non-interpreted info','from the description'] 46 | end 47 | 48 | it 'sets the tags' do 49 | @tags.each do |step| 50 | step.expects(:accept).with visitor 51 | end 52 | visitor.visit_Feature(@node) 53 | end 54 | 55 | it 'iterates over its children' do 56 | @scenarios.each do |scenario| 57 | scenario.expects(:accept).with visitor 58 | end 59 | 60 | visitor.visit_Feature(@node) 61 | end 62 | end 63 | 64 | describe '#visit_Scenario' do 65 | before do 66 | @steps = [ 67 | stub_everything(line: 4), 68 | stub_everything(line: 5), 69 | stub_everything(line: 6) 70 | ] 71 | @tags = [stub_everything, stub_everything, stub_everything] 72 | @node = stub( 73 | tags: @tags, 74 | steps: @steps, 75 | name: 'Go shopping on Saturday morning', 76 | line: 3 77 | ) 78 | end 79 | 80 | it 'adds the scenario to the feature' do 81 | visitor.visit_Scenario(@node) 82 | visitor.feature.scenarios.length.must_equal 1 83 | end 84 | 85 | it 'sets the name' do 86 | visitor.visit_Scenario(@node) 87 | visitor.feature.scenarios.first.name.must_equal 'Go shopping on Saturday morning' 88 | end 89 | 90 | it 'sets the lines' do 91 | visitor.visit_Scenario(@node) 92 | visitor.feature.scenarios.first.lines.must_equal (3..6).to_a 93 | end 94 | 95 | it 'sets the tags' do 96 | @tags.each do |step| 97 | step.expects(:accept).with visitor 98 | end 99 | visitor.visit_Scenario(@node) 100 | end 101 | 102 | it 'iterates over its children' do 103 | @steps.each do |step| 104 | step.expects(:accept).with visitor 105 | end 106 | visitor.visit_Scenario(@node) 107 | end 108 | 109 | end 110 | 111 | describe '#visit_Background' do 112 | before do 113 | @steps = [stub_everything, stub_everything, stub_everything] 114 | @node = stub( 115 | steps: @steps, 116 | line: 3 117 | ) 118 | end 119 | 120 | it 'adds the background to the feature' do 121 | visitor.visit_Background(@node) 122 | visitor.feature.background.must_be_kind_of Background 123 | end 124 | 125 | it 'sets the line' do 126 | visitor.visit_Background(@node) 127 | visitor.feature.background.line.must_equal 3 128 | end 129 | 130 | it 'iterates over its children' do 131 | @steps.each do |step| 132 | step.expects(:accept).with visitor 133 | end 134 | visitor.visit_Background(@node) 135 | end 136 | 137 | it 'visits the background' do 138 | visitor.visit_Background(@node) 139 | end 140 | end 141 | 142 | describe '#visit_Tag' do 143 | it 'adds the tag to the current scenario' do 144 | tags = ['tag1', 'tag2', 'tag3'] 145 | scenario = stub(tags: tags) 146 | visitor.instance_variable_set(:@current_scenario, scenario) 147 | visitor.instance_variable_set(:@current_tag_set, scenario) 148 | 149 | visitor.visit_Tag(stub(name: 'tag4')) 150 | scenario.tags.must_equal ['tag1', 'tag2', 'tag3', 'tag4'] 151 | end 152 | end 153 | 154 | describe '#visit_Step' do 155 | before do 156 | @node = stub(name: 'Baz', line: 3, keyword: 'Given') 157 | @steps = [stub(name: 'Foo'), stub(name: 'Bar')] 158 | end 159 | 160 | it 'adds the step to the step set' do 161 | step_set = stub(steps: @steps) 162 | visitor.instance_variable_set(:@current_step_set, step_set) 163 | 164 | visitor.visit_Step(@node) 165 | step_set.steps.length.must_equal 3 166 | end 167 | 168 | it 'sets the name' do 169 | step_set = stub(steps: []) 170 | visitor.instance_variable_set(:@current_step_set, step_set) 171 | 172 | visitor.visit_Step(@node) 173 | 174 | step_set.steps.first.name.must_equal 'Baz' 175 | end 176 | 177 | it 'sets the keyword' do 178 | step_set = stub(steps: []) 179 | visitor.instance_variable_set(:@current_step_set, step_set) 180 | 181 | visitor.visit_Step(@node) 182 | 183 | step_set.steps.first.keyword.must_equal 'Given' 184 | end 185 | 186 | it 'sets the line' do 187 | step_set = stub(steps: []) 188 | visitor.instance_variable_set(:@current_step_set, step_set) 189 | 190 | visitor.visit_Step(@node) 191 | 192 | step_set.steps.first.line.must_equal 3 193 | end 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /test/spinach/parser_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::Parser do 4 | before do 5 | @contents = """ 6 | Feature: User authentication 7 | As a developer 8 | I expect some stuff 9 | So that I have it 10 | 11 | Scenario: User logs in 12 | Given I am on the front page 13 | When I fill in the login form and press 'login' 14 | Then I should be on my dashboard 15 | """ 16 | @parser = Spinach::Parser.new(@contents) 17 | end 18 | 19 | let(:parsed) { @parser.parse } 20 | 21 | describe '#parse' do 22 | it 'parses the file' do 23 | GherkinRuby.expects(:parse).returns ast = stub 24 | Spinach::Parser::Visitor.stubs(:new).returns visitor = stub 25 | visitor.expects(:visit).with ast 26 | @parser.parse 27 | end 28 | 29 | it 'includes description text in the feature' do 30 | feature = @parser.parse 31 | feature.description.must_equal ['As a developer', 'I expect some stuff', 'So that I have it'] 32 | end 33 | end 34 | 35 | describe '.open_file' do 36 | it 'reads the disk and returns the file content' do 37 | File.expects(:read).with('feature_definition.feature') 38 | @parser = Spinach::Parser.open_file( 39 | 'feature_definition.feature') 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/spinach/reporter/failure_file_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../test_helper' 4 | 5 | describe Spinach::Reporter::FailureFile do 6 | let(:feature) { stub_everything(filename: 'features/test.feature') } 7 | let(:scenario) { stub_everything(lines: [1,2]) } 8 | 9 | describe '#initialize' do 10 | it 'generates a distinctive output filename if none is provided' do 11 | @reporter = Spinach::Reporter::FailureFile.new 12 | @reporter.filename.wont_be_nil 13 | @reporter.filename.must_be_kind_of String 14 | end 15 | 16 | it 'uses a provided output filename option' do 17 | @reporter = Spinach::Reporter::FailureFile.new(failure_filename: 'foo') 18 | @reporter.filename.must_equal 'foo' 19 | end 20 | 21 | it 'uses a provided output filename environment variable' do 22 | ENV['SPINACH_FAILURE_FILE'] = 'asdf' 23 | @reporter = Spinach::Reporter::FailureFile.new 24 | @reporter.filename.must_equal 'asdf' 25 | end 26 | 27 | it 'initializes the array of failing scenarios' do 28 | @reporter = Spinach::Reporter::FailureFile.new 29 | @reporter.failing_scenarios.wont_be_nil 30 | @reporter.failing_scenarios.must_be_kind_of Array 31 | @reporter.failing_scenarios.must_be_empty 32 | end 33 | 34 | after do 35 | ENV.delete('SPINACH_FAILURE_FILE') 36 | end 37 | end 38 | 39 | describe 'hooks' do 40 | before do 41 | @reporter = Spinach::Reporter::FailureFile.new(failure_filename: "tmp/test-failures_#{Time.now.strftime('%F_%H-%M-%S-%L')}.txt") 42 | @reporter.set_current_feature(feature) 43 | @reporter.set_current_scenario(scenario) 44 | end 45 | 46 | describe '#on_failed_step' do 47 | it 'collects the feature and line number for outputting later' do 48 | @reporter.failing_scenarios.must_be_empty 49 | @reporter.on_failed_step(anything) 50 | @reporter.failing_scenarios.must_include "#{feature.filename}:#{scenario.lines[0]}" 51 | end 52 | end 53 | 54 | describe '#on_error_step' do 55 | it 'collects the feature and line number for outputting later' do 56 | @reporter.failing_scenarios.must_be_empty 57 | @reporter.on_error_step(anything) 58 | @reporter.failing_scenarios.must_include "#{feature.filename}:#{scenario.lines[0]}" 59 | end 60 | end 61 | 62 | describe '#after_run' do 63 | describe 'when the run has succeeded' do 64 | it 'no failure file is created' do 65 | @reporter.after_run(true) 66 | refute File.exist?(@reporter.filename), 'Output file should not exist when run is successful' 67 | end 68 | end 69 | 70 | describe 'when the run has failed' do 71 | it 'failure file is created and includes expected output' do 72 | @reporter.on_failed_step(anything) 73 | @reporter.after_run(false) 74 | assert File.exist?(@reporter.filename) 75 | f = File.open(@reporter.filename) 76 | f.read.must_equal "#{feature.filename}:#{scenario.lines[0]}" 77 | f.close 78 | File.unlink(@reporter.filename) 79 | end 80 | end 81 | end 82 | end 83 | 84 | end -------------------------------------------------------------------------------- /test/spinach/reporter/progress_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative '../../test_helper' 4 | 5 | describe Spinach::Reporter::Progress do 6 | let(:exception) { StandardError.new('Something went wrong') } 7 | 8 | let(:error) do 9 | [stub(name: 'My feature'), 10 | stub(name: 'A scenario'), 11 | stub(keyword: 'Keyword', name: 'step name'), 12 | exception] 13 | end 14 | 15 | let(:steps) do 16 | [error] 17 | end 18 | 19 | 20 | before do 21 | @out = StringIO.new 22 | @error = StringIO.new 23 | @reporter = Spinach::Reporter::Progress.new( 24 | output: @out, 25 | error: @error 26 | ) 27 | end 28 | 29 | describe '#on_successful_step' do 30 | let(:step) { stub(keyword: 'Given', name: 'I am too cool') } 31 | let(:step_location){['error_step_location', 1]} 32 | let(:step_definitions){ stub } 33 | 34 | it 'adds the step to the output buffer' do 35 | @reporter.on_successful_step(step, step_location) 36 | 37 | @out.string.must_include '.' 38 | end 39 | 40 | it 'sets the current scenario' do 41 | @reporter.on_successful_step(step, step_location) 42 | 43 | @reporter.scenario.must_include step 44 | end 45 | 46 | it 'adds the step to the successful steps' do 47 | @reporter.on_successful_step(step, step_location) 48 | 49 | @reporter.successful_steps.last.must_include step 50 | end 51 | end 52 | 53 | describe '#on_failed_step' do 54 | let(:step) { stub(keyword: 'Then', name: 'I write failing steps') } 55 | let(:step_location){['error_step_location', 1]} 56 | 57 | it 'adds the step to the output buffer' do 58 | @reporter.on_failed_step(step, anything, step_location) 59 | 60 | @out.string.must_include 'F' 61 | end 62 | 63 | it 'sets the current scenario error' do 64 | @reporter.on_failed_step(step, anything, step_location) 65 | 66 | @reporter.scenario_error.must_include step 67 | end 68 | 69 | it 'adds the step to the failing steps' do 70 | @reporter.on_failed_step(step, anything, step_location) 71 | 72 | @reporter.failed_steps.last.must_include step 73 | end 74 | end 75 | 76 | describe '#on_error_step' do 77 | let(:step) { stub(keyword: 'And', name: 'I even make syntax errors') } 78 | let(:step_location){['error_step_location', 1]} 79 | 80 | it 'adds the step to the output buffer' do 81 | @reporter.on_error_step(step, anything, step_location) 82 | 83 | @out.string.must_include 'E' 84 | end 85 | 86 | it 'sets the current scenario error' do 87 | @reporter.on_error_step(step, anything, step_location) 88 | @reporter.scenario_error.must_include step 89 | end 90 | 91 | it 'adds the step to the error steps' do 92 | @reporter.on_error_step(step, anything, step_location) 93 | @reporter.error_steps.last.must_include step 94 | end 95 | end 96 | 97 | describe '#on_undefined_step' do 98 | let(:step) { stub(keyword: 'When', name: 'I forgot to write steps') } 99 | 100 | it 'adds the step to the output buffer' do 101 | @reporter.on_undefined_step(step, anything) 102 | 103 | @out.string.must_include 'U' 104 | end 105 | 106 | it 'sets the current scenario error' do 107 | @reporter.on_undefined_step(step, anything) 108 | 109 | @reporter.scenario_error.must_include step 110 | end 111 | 112 | it 'adds the step to the undefined steps' do 113 | @reporter.on_undefined_step(step, anything) 114 | 115 | @reporter.undefined_steps.last.must_include step 116 | end 117 | end 118 | 119 | describe '#on_pending_step' do 120 | let(:step) { stub(keyword: 'Given', name: 'I wrote a pending step') } 121 | 122 | it 'adds the step to the output buffer' do 123 | @reporter.on_pending_step(step, anything) 124 | 125 | @out.string.must_include 'P' 126 | end 127 | 128 | it 'sets the current scenario error' do 129 | @reporter.on_pending_step(step, anything) 130 | @reporter.scenario_error.must_include step 131 | end 132 | 133 | it 'adds the step to the pending steps' do 134 | @reporter.on_pending_step(step, anything) 135 | @reporter.pending_steps.last.must_include step 136 | end 137 | end 138 | 139 | describe '#on_skipped_step' do 140 | it 'adds the step to the output buffer' do 141 | @reporter.on_skipped_step(stub(keyword: 'Then', name: 'some steps are not even called')) 142 | 143 | @out.string.must_include '~' 144 | end 145 | end 146 | 147 | describe '#after_run' do 148 | describe 'when the run has succeed' do 149 | it 'display run summary' do 150 | @reporter.expects(:error_summary).never 151 | @reporter.expects(:run_summary) 152 | 153 | @reporter.after_run(true) 154 | end 155 | end 156 | 157 | describe 'when the run has failed' do 158 | it 'display run and error summaries' do 159 | @reporter.expects(:error_summary) 160 | @reporter.expects(:run_summary) 161 | 162 | @reporter.after_run(false) 163 | end 164 | end 165 | end 166 | end 167 | 168 | -------------------------------------------------------------------------------- /test/spinach/reporter_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative '../test_helper' 3 | 4 | module Spinach 5 | describe Reporter do 6 | before do 7 | @options = { 8 | backtrace: true 9 | } 10 | @reporter = Reporter.new(@options) 11 | end 12 | 13 | describe "#options" do 14 | it "returns the options passed to the reporter" do 15 | @reporter.options[:backtrace].must_equal true 16 | end 17 | end 18 | 19 | describe "#current_feature" do 20 | it "returns nil by default" do 21 | @reporter.current_feature.must_equal nil 22 | end 23 | end 24 | 25 | describe "#current_scenario" do 26 | it "returns nil by default" do 27 | @reporter.current_feature.must_equal nil 28 | end 29 | end 30 | 31 | %w{undefined_steps failed_steps error_steps}.each do |errors| 32 | describe "##{errors}" do 33 | it "returns an empty array by default" do 34 | @reporter.send(errors).must_be_empty 35 | end 36 | end 37 | end 38 | 39 | describe "#bind" do 40 | before do 41 | @reporter.stubs( 42 | runner: stub_everything, 43 | feature_runner: stub_everything, 44 | scenario_runner: stub_everything, 45 | ) 46 | @callback = mock 47 | @reporter.stubs(:method) 48 | end 49 | 50 | describe "bindings" do 51 | before do 52 | Spinach.hooks.reset 53 | @reporter.bind 54 | end 55 | 56 | after do 57 | Spinach.hooks.reset 58 | end 59 | 60 | it "binds a callback before running" do 61 | @reporter.expects(:before_run) 62 | Spinach.hooks.run_before_run 63 | end 64 | 65 | it "binds a callback after running" do 66 | @reporter.expects(:after_run) 67 | Spinach.hooks.run_after_run 68 | end 69 | 70 | it "binds a callback before running every feature" do 71 | @reporter.expects(:before_feature_run) 72 | Spinach.hooks.run_before_feature(anything) 73 | end 74 | 75 | it "binds a callback after running every feature" do 76 | @reporter.expects(:after_feature_run) 77 | Spinach.hooks.run_after_feature 78 | end 79 | 80 | it "binds a callback when a feature is not found" do 81 | @reporter.expects(:on_feature_not_found) 82 | Spinach.hooks.run_on_undefined_feature 83 | end 84 | 85 | it "binds a callback before every scenario" do 86 | @reporter.expects(:before_scenario_run) 87 | Spinach.hooks.run_before_scenario(stub_everything) 88 | end 89 | 90 | it "binds a callback around every scenario" do 91 | @reporter.expects(:around_scenario_run) 92 | Spinach.hooks.run_around_scenario(anything) do |&block| 93 | block.call 94 | end 95 | end 96 | 97 | it "yields to around scenario callback" do 98 | called = false 99 | @reporter.around_scenario_run do 100 | called = true 101 | end 102 | called.must_equal true 103 | end 104 | 105 | it "binds a callback after every scenario" do 106 | @reporter.expects(:after_scenario_run) 107 | Spinach.hooks.run_after_scenario 108 | end 109 | 110 | it "binds a callback after every successful step" do 111 | @reporter.expects(:on_successful_step) 112 | Spinach.hooks.run_on_successful_step 113 | end 114 | 115 | it "binds a callback after every failed step" do 116 | @reporter.expects(:on_failed_step) 117 | Spinach.hooks.run_on_failed_step 118 | end 119 | 120 | it "binds a callback after every error step" do 121 | @reporter.expects(:on_error_step) 122 | Spinach.hooks.run_on_error_step 123 | end 124 | 125 | it "binds a callback after every skipped step" do 126 | @reporter.expects(:on_skipped_step) 127 | Spinach.hooks.run_on_skipped_step 128 | end 129 | 130 | describe "internals" do 131 | it "binds a callback before running" do 132 | @reporter.expects(:set_current_feature) 133 | Spinach.hooks.run_before_feature({}) 134 | end 135 | 136 | it "binds a callback after running" do 137 | @reporter.expects(:clear_current_feature) 138 | Spinach.hooks.run_after_feature 139 | end 140 | 141 | it "binds a callback before every scenario" do 142 | @reporter.expects(:set_current_scenario) 143 | Spinach.hooks.run_before_scenario 144 | end 145 | 146 | it "binds a callback after every scenario" do 147 | @reporter.expects(:clear_current_scenario) 148 | Spinach.hooks.run_after_scenario 149 | end 150 | end 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /test/spinach/scenario_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Spinach 4 | describe Scenario do 5 | describe "#ordering_id" do 6 | let(:feature) { Feature.new } 7 | 8 | subject { Scenario.new(feature) } 9 | 10 | before do 11 | feature.filename = "features/foo/bar.feature" 12 | subject.lines = Array(4..12) 13 | end 14 | 15 | it 'is the filename and starting line number' do 16 | subject.ordering_id.must_equal "features/foo/bar.feature:4" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/spinach/step_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Spinach 4 | describe Step do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/spinach/support_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::Support do 4 | describe '#camelize' do 5 | it 'returns an empty string with nil values' do 6 | Spinach::Support.camelize(nil).must_equal '' 7 | end 8 | 9 | it 'downcases the given value' do 10 | Spinach::Support.camelize('VALUE').must_equal 'Value' 11 | end 12 | 13 | it 'squeezes the spaces for the given value' do 14 | Spinach::Support.camelize('feature name').must_equal 'FeatureName' 15 | end 16 | 17 | it 'strips the given value' do 18 | Spinach::Support.camelize(' value ').must_equal 'Value' 19 | end 20 | 21 | it 'strips the given value' do 22 | Spinach::Support.camelize('feature name').must_equal 'FeatureName' 23 | end 24 | end 25 | 26 | describe '#scoped_camelize' do 27 | it 'prepends a scope to the class' do 28 | Spinach::Support.scoped_camelize('feature name').must_equal 'Spinach::Features::FeatureName' 29 | end 30 | end 31 | 32 | describe '#underscore' do 33 | it 'changes dashes to underscores' do 34 | Spinach::Support.underscore('feature-name').must_equal 'feature_name' 35 | end 36 | 37 | it 'downcases the text' do 38 | Spinach::Support.underscore('FEATURE').must_equal 'feature' 39 | end 40 | 41 | it 'converts namespaces to paths' do 42 | Spinach::Support.underscore('Spinach::Support').must_equal 'spinach/support' 43 | end 44 | 45 | it 'prepends underscores to uppercase letters' do 46 | Spinach::Support.underscore('FeatureName').must_equal 'feature_name' 47 | end 48 | 49 | it 'only prepends underscores to the last uppercase letter' do 50 | Spinach::Support.underscore('SSLError').must_equal 'ssl_error' 51 | end 52 | 53 | it 'does not modify the original string' do 54 | text = 'FeatureName' 55 | underscored_text = Spinach::Support.underscore(text) 56 | 57 | text.wont_equal underscored_text 58 | end 59 | 60 | it 'accepts non string values' do 61 | Spinach::Support.underscore(:FeatureName).must_equal 'feature_name' 62 | end 63 | 64 | it 'changes spaces to underscores' do 65 | Spinach::Support.underscore('feature name').must_equal 'feature_name' 66 | end 67 | end 68 | 69 | describe "#escape" do 70 | it "escapes the name" do 71 | Spinach::Support.escape_single_commas( 72 | "I've been doing things I shouldn't be doing" 73 | ).must_include "I\\'ve been doing things I shouldn\\'t be doing" 74 | end 75 | end 76 | 77 | describe '#constantize' do 78 | it "converts a string into a class" do 79 | Spinach::Support.constantize("Spinach::FeatureSteps").must_equal Spinach::FeatureSteps 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/spinach/tags_matcher_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | describe Spinach::TagsMatcher do 4 | 5 | describe '#match' do 6 | 7 | before do 8 | @config = Spinach::Config.new 9 | Spinach.stubs(:config).returns(@config) 10 | end 11 | 12 | subject { Spinach::TagsMatcher } 13 | 14 | describe "when matching against a single tag" do 15 | 16 | before { @config.tags = [['wip']] } 17 | 18 | it "matches the same tag" do 19 | subject.match(['wip']).must_equal true 20 | end 21 | 22 | it "does not match a different tag" do 23 | subject.match(['important']).must_equal false 24 | end 25 | 26 | it "does not match when no tags are present" do 27 | subject.match([]).must_equal false 28 | end 29 | end 30 | 31 | describe 'when matching against a single negated tag' do 32 | 33 | before { @config.tags = [['~wip']] } 34 | 35 | it "returns false for the same tag" do 36 | subject.match(['wip']).must_equal false 37 | end 38 | 39 | it "returns true for a different tag" do 40 | subject.match(['important']).must_equal true 41 | end 42 | 43 | it "returns true when no tags are present" do 44 | subject.match([]).must_equal true 45 | end 46 | end 47 | 48 | describe 'when matching against a single negated tag and an added tag' do 49 | 50 | before { @config.tags = [['~wip', 'added']] } 51 | 52 | it "returns false for the same tag" do 53 | subject.match(['wip']).must_equal false 54 | end 55 | 56 | it "returns true for the added tag" do 57 | subject.match(['added']).must_equal true 58 | end 59 | 60 | it "returns false for a different tag" do 61 | subject.match(['important']).must_equal false 62 | end 63 | 64 | it "returns false when no tags are present" do 65 | subject.match([]).must_equal false 66 | end 67 | end 68 | 69 | describe "when matching against ANDed tags" do 70 | 71 | before { @config.tags = [['wip'], ['important']] } 72 | 73 | it "returns true when all tags match" do 74 | subject.match(['wip', 'important']).must_equal true 75 | end 76 | 77 | it "returns false when one tag matches" do 78 | subject.match(['important']).must_equal false 79 | end 80 | 81 | it "returns false when no tags match" do 82 | subject.match(['foo']).must_equal false 83 | end 84 | 85 | it "returns false when no tags are present" do 86 | subject.match([]).must_equal false 87 | end 88 | end 89 | 90 | describe "when matching against ORed tags" do 91 | 92 | before { @config.tags = [['wip', 'important']] } 93 | 94 | it "returns true when all tags match" do 95 | subject.match(['wip', 'important']).must_equal true 96 | end 97 | 98 | it "returns true when one tag matches" do 99 | subject.match(['important']).must_equal true 100 | end 101 | 102 | it "returns false when no tags match" do 103 | subject.match(['foo']).must_equal false 104 | end 105 | 106 | it "returns false when no tags are present" do 107 | subject.match([]).must_equal false 108 | end 109 | end 110 | 111 | describe 'when matching against combined ORed and ANDed tags' do 112 | 113 | before { @config.tags = [['billing', 'wip'], ['important']] } 114 | 115 | it "returns true when all tags match" do 116 | subject.match(['billing', 'wip', 'important']).must_equal true 117 | end 118 | 119 | it "returns true when tags from both AND-groups match" do 120 | subject.match(['wip', 'important']).must_equal true 121 | subject.match(['billing', 'important']).must_equal true 122 | end 123 | 124 | it "returns false when tags from one AND-group match" do 125 | subject.match(['important']).must_equal false 126 | subject.match(['billing']).must_equal false 127 | end 128 | 129 | it "returns false when no tags match" do 130 | subject.match(['foo']).must_equal false 131 | end 132 | 133 | it "returns false when no tags are present" do 134 | subject.match([]).must_equal false 135 | end 136 | end 137 | 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/spinach_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | describe Spinach do 4 | before do 5 | @feature_steps1 = OpenStruct.new(feature_name: 'User authentication') 6 | @feature_steps2 = OpenStruct.new(feature_name: 'Slip management') 7 | @feature_steps3 = OpenStruct.new(feature_name: 'File attachments') 8 | @feature_steps4 = OpenStruct.new(name: 'UserSendsAMessage') 9 | @feature_steps5 = OpenStruct.new(name: 'Spinach::Features::ScopedFeature') 10 | [@feature_steps1, @feature_steps2, 11 | @feature_steps3, @feature_steps4, @feature_steps5].each do |feature| 12 | Spinach.feature_steps << feature 13 | end 14 | end 15 | 16 | describe '#features' do 17 | it 'returns all the loaded features' do 18 | Spinach.feature_steps.must_include @feature_steps1 19 | Spinach.feature_steps.must_include @feature_steps2 20 | Spinach.feature_steps.must_include @feature_steps3 21 | end 22 | end 23 | 24 | describe '#reset_features' do 25 | it 'resets the features to a pristine state' do 26 | Spinach.reset_feature_steps 27 | [@feature_steps1, @feature_steps3, @feature_steps3].each do |feature| 28 | Spinach.feature_steps.wont_include feature 29 | end 30 | end 31 | end 32 | 33 | describe '#find_step_definitions' do 34 | it 'finds a feature by name' do 35 | Spinach.find_step_definitions('User authentication').must_equal @feature_steps1 36 | Spinach.find_step_definitions('Slip management').must_equal @feature_steps2 37 | Spinach.find_step_definitions('File attachments').must_equal @feature_steps3 38 | end 39 | 40 | describe 'when a feature class does not set a feature_name' do 41 | it 'guesses the feature class from the feature name' do 42 | Spinach.find_step_definitions('User sends a message').must_equal @feature_steps4 43 | end 44 | 45 | it 'finds scoped features' do 46 | Spinach.find_step_definitions('Scoped feature').must_equal @feature_steps5 47 | end 48 | 49 | it 'returns nil when it cannot find the class' do 50 | Spinach.find_step_definitions('This feature does not exist').must_equal nil 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/support/filesystem.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | module Filesystem 4 | def in_current_dir(&block) 5 | dir = "tmp/fs" 6 | FileUtils.mkdir_p(dir) 7 | Dir.chdir(dir, &block) 8 | ensure 9 | FileUtils.rm_rf(dir) 10 | end 11 | end 12 | 13 | MiniTest::Spec.send(:include, Filesystem) 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/spec' 3 | require 'mocha/minitest' 4 | require 'ostruct' 5 | require 'stringio' 6 | require 'pry' 7 | require 'fakefs/safe' 8 | 9 | require 'spinach' 10 | 11 | require_relative "support/filesystem" 12 | 13 | module Kernel 14 | def capture_stdout 15 | out = StringIO.new 16 | $stdout = out 17 | $stderr = out 18 | yield 19 | return out.string 20 | ensure 21 | $stdout = STDOUT 22 | $stdout = STDERR 23 | end 24 | end 25 | --------------------------------------------------------------------------------