In the unlikely event that anyone wants to scroll through the whole history of the TDD demo Rails app I did the other day, I wrote a little script to munge it all into one page. And here it is.
New Rails project rails new todo-rails rails --skip-spring --skip-test-unit I told it not to install test-unit as we'll be using rspec instead, and not to install spring (a preloader to speed up test runs) because sometimes it doesn't quite work as expected, or you forget you need to reload it when changing certain things, leading to confusing test failures.
Replace README.rdoc with .mdREADME.md
+# Sample "to-do" Rails app + +Created for a demo of TDD with Rails, using RSpec and Cucumber. + +See full commit messages for step-by-step description. Where files were changed +by running a command rather than manually, that step will be a separate commit, +with the command(s) listed in the commit message.README.rdoc
-== README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... - - -Please feel free to use a different markup language if you do not plan to run -<tt>rake doc:app</tt>.
Add links to readmeREADME.md
# Sample "to-do" Rails app -Created for a demo of TDD with Rails, using RSpec and Cucumber. +Created for a demo of TDD with [http://rubyonrails.org/](Rails), using +[http://rspec.info/](RSpec) and [https://cukes.info/](Cucumber). See full commit messages for step-by-step description. Where files were changed by running a command rather than manually, that step will be a separate commit, with the command(s) listed in the commit message. + +To keep things simple, we're not using any additional testing tools apart from +RSpec and Cucumber (apart from Database Cleaner and Capybara, which Cucumber +depends on by default). In a real project, some or all of the following might +be useful: + +* [factory_girl](https://github.com/thoughtbot/factory_girl) for building test + data +* [faker](https://github.com/stympy/faker) for more realistic test values +* [SitePrism](https://github.com/natritmeyer/site_prism) to describe your site + using the [page object model](http://martinfowler.com/bliki/PageObject.html) + pattern +* [timecop](https://github.com/travisjeffery/timecop) to test time-dependent + code +* [poltergeist](https://github.com/teampoltergeist/poltergeist) to use + capybara on pages that use javascript +* [capybara-screenshot](https://github.com/mattheworiordan/capybara-screenshot) + to automatically open a screenshot of the current page when a test fails
Tidy up GemfileGemfile
source 'https://rubygems.org' - -# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.0' -# Use sqlite3 as the database for Active Record -gem 'sqlite3' -# Use SCSS for stylesheets -gem 'sass-rails', '~> 5.0' -# Use Uglifier as compressor for JavaScript assets -gem 'uglifier', '>= 1.3.0' -# Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.1.0' -# See https://github.com/sstephenson/execjs#readme for more supported runtimes -# gem 'therubyracer', platforms: :ruby - -# Use jquery as the JavaScript library -gem 'jquery-rails' -# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks -gem 'turbolinks' -# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.0' -# bundle exec rake doc:rails generates the API under doc/api. +gem 'jquery-rails' +gem 'rails', '4.2.0' +gem 'sass-rails', '~> 5.0' gem 'sdoc', '~> 0.4.0', group: :doc - -# Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' - -# Use Unicorn as the app server -# gem 'unicorn' - -# Use Capistrano for deployment -# gem 'capistrano-rails', group: :development +gem 'sqlite3' +gem 'turbolinks' +gem 'uglifier', '>= 1.3.0' group :development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' - - # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '~> 2.0' end
Remove version restrictions from Gemfile The exact versions are in Gemfile.lock anyway, and if at some point a bundle update breaks something we can always lock versions of specific gems.Gemfile
source 'https://rubygems.org' -gem 'coffee-rails', '~> 4.1.0' -gem 'jbuilder', '~> 2.0' +gem 'coffee-rails' +gem 'jbuilder' gem 'jquery-rails' -gem 'rails', '4.2.0' -gem 'sass-rails', '~> 5.0' -gem 'sdoc', '~> 0.4.0', group: :doc +gem 'rails' +gem 'sass-rails' +gem 'sdoc' gem 'sqlite3' gem 'turbolinks' -gem 'uglifier', '>= 1.3.0' +gem 'uglifier' group :development, :test do gem 'byebug' - gem 'web-console', '~> 2.0' + gem 'web-console' end -
Remove a couple of gems we aren't going to useGemfile
source 'https://rubygems.org' gem 'coffee-rails' -gem 'jbuilder' gem 'jquery-rails' gem 'rails' gem 'sass-rails' -gem 'sdoc' gem 'sqlite3' gem 'turbolinks' gem 'uglifier' group :development, :test do gem 'byebug' gem 'web-console' endGemfile.lock
GEM remote: https://rubygems.org/ specs: actionmailer (4.2.0) actionpack (= 4.2.0) actionview (= 4.2.0) activejob (= 4.2.0) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) actionpack (4.2.0) actionview (= 4.2.0) activesupport (= 4.2.0) rack (~> 1.6.0) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) actionview (4.2.0) activesupport (= 4.2.0) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) activejob (4.2.0) activesupport (= 4.2.0) globalid (>= 0.3.0) activemodel (4.2.0) activesupport (= 4.2.0) builder (~> 3.1) activerecord (4.2.0) activemodel (= 4.2.0) activesupport (= 4.2.0) arel (~> 6.0) activesupport (4.2.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) arel (6.0.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) byebug (4.0.3) columnize (= 0.9.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) coffee-script (2.3.0) coffee-script-source execjs coffee-script-source (1.9.1) columnize (0.9.0) debug_inspector (0.0.2) erubis (2.7.0) execjs (2.4.0) globalid (0.3.3) activesupport (>= 4.1.0) hike (1.2.3) i18n (0.7.0) - jbuilder (2.2.12) - activesupport (>= 3.0.0, < 5) - multi_json (~> 1.2) jquery-rails (4.0.3) rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.2) loofah (2.0.1) nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) mime-types (2.4.3) mini_portile (0.6.2) minitest (5.5.1) multi_json (1.11.0) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) rack (1.6.0) rack-test (0.6.3) rack (>= 1.0) rails (4.2.0) actionmailer (= 4.2.0) actionpack (= 4.2.0) actionview (= 4.2.0) activejob (= 4.2.0) activemodel (= 4.2.0) activerecord (= 4.2.0) activesupport (= 4.2.0) bundler (>= 1.3.0, < 2.0) railties (= 4.2.0) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (1.0.6) activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) railties (4.2.0) actionpack (= 4.2.0) activesupport (= 4.2.0) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.4.2) - rdoc (4.2.0) sass (3.4.13) sass-rails (5.0.1) railties (>= 4.0.0, < 5.0) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (~> 1.1) - sdoc (0.4.1) - json (~> 1.7, >= 1.7.7) - rdoc (~> 4.0) sprockets (2.12.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) sprockets-rails (2.2.4) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.10) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) turbolinks (2.5.3) coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) web-console (2.1.2) activemodel (>= 4.0) binding_of_caller (>= 0.7.2) railties (>= 4.0) sprockets-rails (>= 2.0, < 4.0) PLATFORMS ruby DEPENDENCIES byebug - coffee-rails (~> 4.1.0) - jbuilder (~> 2.0) + coffee-rails jquery-rails - rails (= 4.2.0) - sass-rails (~> 5.0) - sdoc (~> 0.4.0) + rails + sass-rails sqlite3 turbolinks - uglifier (>= 1.3.0) - web-console (~> 2.0) + uglifier + web-console
Switch to double quotes in Gemfile Just personal preference!Gemfile
-source 'https://rubygems.org' +source "https://rubygems.org" -gem 'coffee-rails' -gem 'jquery-rails' -gem 'rails' -gem 'sass-rails' -gem 'sqlite3' -gem 'turbolinks' -gem 'uglifier' +gem "coffee-rails" +gem "jquery-rails" +gem "rails" +gem "sass-rails" +gem "sqlite3" +gem "turbolinks" +gem "uglifier" group :development, :test do - gem 'byebug' - gem 'web-console' + gem "byebug" + gem "web-console" end
Add rspec-rails gemGemfile
source "https://rubygems.org" gem "coffee-rails" gem "jquery-rails" gem "rails" gem "sass-rails" gem "sqlite3" gem "turbolinks" gem "uglifier" group :development, :test do gem "byebug" + gem "rspec-rails" gem "web-console" endGemfile.lock
GEM remote: https://rubygems.org/ specs: actionmailer (4.2.0) actionpack (= 4.2.0) actionview (= 4.2.0) activejob (= 4.2.0) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) actionpack (4.2.0) actionview (= 4.2.0) activesupport (= 4.2.0) rack (~> 1.6.0) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) actionview (4.2.0) activesupport (= 4.2.0) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) activejob (4.2.0) activesupport (= 4.2.0) globalid (>= 0.3.0) activemodel (4.2.0) activesupport (= 4.2.0) builder (~> 3.1) activerecord (4.2.0) activemodel (= 4.2.0) activesupport (= 4.2.0) arel (~> 6.0) activesupport (4.2.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) arel (6.0.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) byebug (4.0.3) columnize (= 0.9.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) coffee-script (2.3.0) coffee-script-source execjs coffee-script-source (1.9.1) columnize (0.9.0) debug_inspector (0.0.2) + diff-lcs (1.2.5) erubis (2.7.0) execjs (2.4.0) globalid (0.3.3) activesupport (>= 4.1.0) hike (1.2.3) i18n (0.7.0) jquery-rails (4.0.3) rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.2) loofah (2.0.1) nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) mime-types (2.4.3) mini_portile (0.6.2) minitest (5.5.1) multi_json (1.11.0) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) rack (1.6.0) rack-test (0.6.3) rack (>= 1.0) rails (4.2.0) actionmailer (= 4.2.0) actionpack (= 4.2.0) actionview (= 4.2.0) activejob (= 4.2.0) activemodel (= 4.2.0) activerecord (= 4.2.0) activesupport (= 4.2.0) bundler (>= 1.3.0, < 2.0) railties (= 4.2.0) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (1.0.6) activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) railties (4.2.0) actionpack (= 4.2.0) activesupport (= 4.2.0) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.4.2) + rspec-core (3.2.2) + rspec-support (~> 3.2.0) + rspec-expectations (3.2.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-mocks (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-rails (3.2.1) + actionpack (>= 3.0, < 4.3) + activesupport (>= 3.0, < 4.3) + railties (>= 3.0, < 4.3) + rspec-core (~> 3.2.0) + rspec-expectations (~> 3.2.0) + rspec-mocks (~> 3.2.0) + rspec-support (~> 3.2.0) + rspec-support (3.2.2) sass (3.4.13) sass-rails (5.0.1) railties (>= 4.0.0, < 5.0) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (~> 1.1) sprockets (2.12.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) sprockets-rails (2.2.4) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.10) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) turbolinks (2.5.3) coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) web-console (2.1.2) activemodel (>= 4.0) binding_of_caller (>= 0.7.2) railties (>= 4.0) sprockets-rails (>= 2.0, < 4.0) PLATFORMS ruby DEPENDENCIES byebug coffee-rails jquery-rails rails + rspec-rails sass-rails sqlite3 turbolinks uglifier web-console
Set up rspec rails g rspec:install.rspec
+--color +--require spec_helperspec/rails_helper.rb
+# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require 'spec_helper' +require File.expand_path('../../config/environment', __FILE__) +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migrations before tests are run. +# If you are not using ActiveRecord, you can remove this line. +ActiveRecord::Migration.maintain_test_schema! + +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! +endspec/spec_helper.rb
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax + # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end
Add cucumber-rails and database_cleaner gemsGemfile
source "https://rubygems.org" gem "coffee-rails" gem "jquery-rails" gem "rails" gem "sass-rails" gem "sqlite3" gem "turbolinks" gem "uglifier" group :development, :test do gem "byebug" gem "rspec-rails" gem "web-console" end + +group :test do + gem "cucumber-rails", require: false + gem "database_cleaner" +endGemfile.lock
GEM remote: https://rubygems.org/ specs: actionmailer (4.2.0) actionpack (= 4.2.0) actionview (= 4.2.0) activejob (= 4.2.0) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) actionpack (4.2.0) actionview (= 4.2.0) activesupport (= 4.2.0) rack (~> 1.6.0) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) actionview (4.2.0) activesupport (= 4.2.0) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) activejob (4.2.0) activesupport (= 4.2.0) globalid (>= 0.3.0) activemodel (4.2.0) activesupport (= 4.2.0) builder (~> 3.1) activerecord (4.2.0) activemodel (= 4.2.0) activesupport (= 4.2.0) arel (~> 6.0) activesupport (4.2.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) arel (6.0.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) byebug (4.0.3) columnize (= 0.9.0) + capybara (2.4.4) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) coffee-script (2.3.0) coffee-script-source execjs coffee-script-source (1.9.1) columnize (0.9.0) + cucumber (1.3.19) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.2) + cucumber-rails (1.4.2) + capybara (>= 1.1.2, < 3) + cucumber (>= 1.3.8, < 2) + mime-types (>= 1.16, < 3) + nokogiri (~> 1.5) + rails (>= 3, < 5) + database_cleaner (1.4.0) debug_inspector (0.0.2) diff-lcs (1.2.5) erubis (2.7.0) execjs (2.4.0) + gherkin (2.12.2) + multi_json (~> 1.3) globalid (0.3.3) activesupport (>= 4.1.0) hike (1.2.3) i18n (0.7.0) jquery-rails (4.0.3) rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.2) loofah (2.0.1) nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) mime-types (2.4.3) mini_portile (0.6.2) minitest (5.5.1) multi_json (1.11.0) + multi_test (0.1.2) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) rack (1.6.0) rack-test (0.6.3) rack (>= 1.0) rails (4.2.0) actionmailer (= 4.2.0) actionpack (= 4.2.0) actionview (= 4.2.0) activejob (= 4.2.0) activemodel (= 4.2.0) activerecord (= 4.2.0) activesupport (= 4.2.0) bundler (>= 1.3.0, < 2.0) railties (= 4.2.0) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (1.0.6) activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.2) loofah (~> 2.0) railties (4.2.0) actionpack (= 4.2.0) activesupport (= 4.2.0) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.4.2) rspec-core (3.2.2) rspec-support (~> 3.2.0) rspec-expectations (3.2.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.2.0) rspec-mocks (3.2.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.2.0) rspec-rails (3.2.1) actionpack (>= 3.0, < 4.3) activesupport (>= 3.0, < 4.3) railties (>= 3.0, < 4.3) rspec-core (~> 3.2.0) rspec-expectations (~> 3.2.0) rspec-mocks (~> 3.2.0) rspec-support (~> 3.2.0) rspec-support (3.2.2) sass (3.4.13) sass-rails (5.0.1) railties (>= 4.0.0, < 5.0) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (~> 1.1) sprockets (2.12.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) sprockets-rails (2.2.4) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.10) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) turbolinks (2.5.3) coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) web-console (2.1.2) activemodel (>= 4.0) binding_of_caller (>= 0.7.2) railties (>= 4.0) sprockets-rails (>= 2.0, < 4.0) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES byebug coffee-rails + cucumber-rails + database_cleaner jquery-rails rails rspec-rails sass-rails sqlite3 turbolinks uglifier web-console
Set up cucumber rails g cucumber:installconfig/cucumber.yml
+<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +%> +default: <%= std_opts %> features +wip: --tags @wip:3 --wip features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wipconfig/database.yml
# SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' # default: &default adapter: sqlite3 pool: 5 timeout: 5000 development: <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. -test: +test: &test <<: *default database: db/test.sqlite3 production: <<: *default database: db/production.sqlite3 + +cucumber: + <<: *test \ No newline at end of filefeatures/step_definitions/.gitkeep
features/support/env.rb
+# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + +require 'cucumber/rails' + +# Capybara defaults to CSS3 selectors rather than XPath. +# If you'd prefer to use XPath, just uncomment this line and adjust any +# selectors in your step definitions to use the XPath syntax. +# Capybara.default_selector = :xpath + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + DatabaseCleaner.strategy = :transaction +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { :except => [:widgets] } may not do what you expect here +# # as Cucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation +lib/tasks/cucumber.rake
+# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({:ok => 'test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({:wip => 'test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({:rerun => 'test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task :all => [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + end + desc 'Alias for cucumber:ok' + task :cucumber => 'cucumber:ok' + + task :default => :cucumber + + task :features => :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. + task 'test:prepare' do + end + + task :stats => 'cucumber:statsetup' +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +endscript/cucumber
+#!/usr/bin/env ruby + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end
Add cucumber scenario for viewing to-do list We mark the scenario as @wip (work-in-progress) to indicate that it isn't yet implemented, and shouldn't fail the build if it fails. Speaking of which, to run the build: rake You can ignore the message about the database not existing for now – we'll get to that shortly. If you haven't done so already, you'll need to install the additional gems we've added first: bundlefeatures/todo_list.feature
+Feature: to-do list + + @wip + Scenario: See what I've got to do + Given I have some items in my to-do list + When I go to the home page + Then I can see all the items
Explicitly specify tasks for default rake build We just want it to run the following tasks for now: * Run any pending databse migrations * Run the specs (unit tests) * Run the main cucumber features (acceptance/regression test) * Run the work-in-progress cucumber features The "cucumber:wip" task runs features and scenarios tagged as @wip separately, and does not fail the build if they fail. In fact, it *does* fail the build if they succeed, as a reminder to remove the @wip tag.lib/tasks/default.rake
+Rake::Task[:default].prerequisites.clear if Rake::Task.task_defined?(:default) + +task :default => [ + :"db:migrate", + :spec, + :cucumber, + :"cucumber:wip", +]
Commit schema.rb This file was created when we ran rake, and will be updated whenever we migrate the database (to add tables etc). It is used to set up a new schema from scratch (rather than running through every migration ever), which is why it should be checked into version control.db/schema.rb
+# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 0) do + +end
Add failing definition for the first scenario step Incidentally, you can just run the WIP scenarios like this, rather than running rake: bundle exec cucumber -p wipfeatures/step_definitions/todo_list_steps.rb
+Given "I have some items in my to-do list" do + @item_1 = Item.create name: "Make a Rails app" + @item_2 = Item.create name: "Demonstrate test-driven development" +end +
Add Item class Step still fails, because there's no associated database table yet.app/models/item.rb
+class Item < ActiveRecord::Base +end
Add a migration to create the items table bin/rails g migration CreateItem name:string rake Now our first "Given" step succeeds!db/migrate/20150321115919_create_item.rb
+class CreateItem < ActiveRecord::Migration + def change + create_table :items do |t| + t.string :name + end + end +enddb/schema.rb
# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # Note that this schema.rb definition is the authoritative source for your # database schema. If you need to create the application database on another # system, you should be using db:schema:load, not running all the migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 20150321115919) do + + create_table "items", force: :cascade do |t| + t.string "name" + end end
Implement failing step for visiting home page We try to visit the root (home) page, but get this error: undefined local variable or method `root_path' ... This is because we haven't set up a root route yet. We'll do that next.features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end +When "I go to the home page" do + visit root_path +end
Add a (non-working) root route When we run the feature now, the message changes: uninitialized constant ItemsController (ActionController::RoutingError)config/routes.rb
Rails.application.routes.draw do - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end + root to: "items#index" end
Add an empty controller Once again, this fixes the previous error, but causes another: The action 'index' could not be found for ItemsController ... Note how we're going in small steps, making just enough of a change each time to fix the specific thing that the test complains about, rather than trying to do everything at once. This is what Bryan Liles calls "make it pass or change the message".app/controllers/items_controller.rb
+class ItemsController < ApplicationController +end
Add an empty index action to the items controller Once again, a small step and a new error: Missing template items/index, application/index ...app/controllers/items_controller.rb
class ItemsController < ApplicationController + def index + end end
Add an items index view Now our step finally succeeds, although there's nothing on the page yet.app/views/items/index.html.erb
Mention haml (which we're not using) in the readmeREADME.md
# Sample "to-do" Rails app Created for a demo of TDD with [http://rubyonrails.org/](Rails), using [http://rspec.info/](RSpec) and [https://cukes.info/](Cucumber). See full commit messages for step-by-step description. Where files were changed by running a command rather than manually, that step will be a separate commit, with the command(s) listed in the commit message. To keep things simple, we're not using any additional testing tools apart from RSpec and Cucumber (apart from Database Cleaner and Capybara, which Cucumber depends on by default). In a real project, some or all of the following might be useful: * [factory_girl](https://github.com/thoughtbot/factory_girl) for building test data * [faker](https://github.com/stympy/faker) for more realistic test values * [SitePrism](https://github.com/natritmeyer/site_prism) to describe your site using the [page object model](http://martinfowler.com/bliki/PageObject.html) pattern * [timecop](https://github.com/travisjeffery/timecop) to test time-dependent code * [poltergeist](https://github.com/teampoltergeist/poltergeist) to use capybara on pages that use javascript * [capybara-screenshot](https://github.com/mattheworiordan/capybara-screenshot) to automatically open a screenshot of the current page when a test fails + +Personally, I'd also use [haml](http://haml.info/) instead of erb for the view +templating, but we're sticking with the default here for simplicity.
Change application title in layoutapp/views/layouts/application.html.erb
<!DOCTYPE html> <html> <head> - <title>TodoRails</title> + <title>To-do list</title> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
Implement step to check that items appear in listfeatures/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "I go to the home page" do visit root_path end + +Then "I can see all the items" do + expect(page).to have_css "ul li", text: @item_1.name + expect(page).to have_css "ul li", text: @item_2.name +end
Start a failing spec for ItemsController#index Start by just calling the action and checking the outcome we want. Obviously this fails because we haven't set up anything up yet: undefined local variable or method `items' ... Normally you wouldn't want to commit a failing spec, but I've done it here to show the small steps we're taking. As a compromise, the failing changes are in a branch that will be merged in once it's green, so at least if we follow the git history down the first parent everything will build cleanly.spec/controllers/items_controller_spec.rb
+require "rails_helper" + +describe ItemsController do + describe "GET index" do + it "assigns all the items for the view" do + get :index + expect(assigns[:items]).to eq items + end + end +end
Create an "items" variable Spec now fails "properly" (ie with a failed expectation rather than an undefined variable): expected: [#<RSpec::Mocks::Double:0x3fe4aedcab40 @name=:item>] got: nilspec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do + let(:items) { [double(:item)] } + it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end end
Extract the item checks to a matcher This allows us to keep the knowledge about how an item is displayed in one place, rather than duplicating it in any steps that need to check (even with this one step definition there was duplication, with the same check for both items). If we later change the way items are shown (maybe by using a table rather than an unordered list), we only have to make one change in the tests, rather than having to find everywhere we look for items.features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "I go to the home page" do visit root_path end Then "I can see all the items" do - expect(page).to have_css "ul li", text: @item_1.name - expect(page).to have_css "ul li", text: @item_2.name + expect(page).to show_todo_item @item_1 + expect(page).to show_todo_item @item_2 endfeatures/support/todo_list_step_helper.rb
+require "rspec/expectations" + +RSpec::Matchers.define :show_todo_item do |item| + match do |page| + page.has_css? "li", text: item.name + end + + failure_message { %(Expected page to contain a list item "#{item.name}") } +end
Stub query on Item This doesn't change the failure message, but we now have everything in place for the behaviour we want to implement.spec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } + before do + allow(Item).to receive(:all) { items } + end + it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end end
Hard-code part of the expected output This is just a quick sanity check to make sure we're testing for the right thing. Now we've confirmed that the test is working (it now finds the first item, and fails looking for the second), we can go on to implement it properly using the actual values from the database.app/views/items/index.html.erb
+<ul> + <li>Make a Rails app</li> +</ul>
Implement index method to pass test This also makes the feature pass, so we remove the WIP tag.app/controllers/items_controller.rb
class ItemsController < ApplicationController def index + @items = Item.all end endfeatures/todo_list.feature
Feature: to-do list - @wip Scenario: See what I've got to do Given I have some items in my to-do list When I go to the home page Then I can see all the items
Implement the items view properly Still fails, because we aren't passing a list of items to the view from the controller.app/views/items/_item.html.erb
+<li><%= item.name %></li>app/views/items/index.html.erb
<ul> - <li>Make a Rails app</li> + <%= render @items %> </ul>
Add "add item" form to home page This breaks the app because there's no route yet, so we'll work on a branch for a bit again.app/views/items/index.html.erb
<ul> <%= render @items %> </ul> +<%= form_for Item.new do |f| %> + <%= f.label :name, "New item:" %> + <%= f.text_field :name %> + <%= f.submit "Add" %> +<% end %>
Add routing resource for items The new item form now displays correctly, but when we try to submit it in the feature we get an error: No route matches [POST] "/items" ...config/routes.rb
Rails.application.routes.draw do + resources :items, only: %i(index) root to: "items#index" end
Add create action to items resource Once again, we move the error message on slightly: The action 'create' could not be found for ItemsController ...config/routes.rb
Rails.application.routes.draw do - resources :items, only: %i(index) + resources :items, only: %i(index create) root to: "items#index" end
Add empty create action Change the error message again: Missing template items/create, application/create ...app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end + + def create + end end
Add WIP scenario for adding an itemfeatures/todo_list.feature
Feature: to-do list Scenario: See what I've got to do Given I have some items in my to-do list When I go to the home page Then I can see all the items + + @wip + Scenario: Add an item to my to-do list + When I add a to-do item + Then it appears in the list
Failing spec for create redirecting back to index Let's ignore the actual item creation for now, because the current feature failure is caused by the controller trying to render a non-existent template (the default behaviour is to render a template matching the action name).spec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } before do allow(Item).to receive(:all) { items } end it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end + + describe "POST create" do + it "redirects to the index" do + post :create + expect(response).to redirect_to action: :index + end + end end
Add step definition for adding an item It calls a helper method that doesn't exist yet, which is fine -- it lets us figure out what we need to implement. This is called "programming by wishful thinking" (as far as I can tell, this was first mentioned in the classic book "Structure and Interpretation of Computer Programs").features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "I go to the home page" do visit root_path end +When "I add a to-do item" do + visit root_path + add_item "Make this test pass" +end + Then "I can see all the items" do expect(page).to show_todo_item @item_1 expect(page).to show_todo_item @item_2 end
Implement the redirect This fixes the failing feature, so now we can go on to implement the last step, which will check that the item gets created.app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create + redirect_to action: :index end end
Implement add_item step helper Cucumber helper methods are added to modules, which are mixed into cucumber's world using the World method Cucumber helper methods are added to modules, which are mixed into cucumber's world using the "World" method. Although we could just define the methods at the top level, using modules allows us to keep them organised, and gives more helpful stack traces when tests fail.features/support/todo_list_step_helper.rb
require "rspec/expectations" +module TodoListStepHelper + def add_item name + fill_in "New item:", with: name + click_on "Add" + end +end +World TodoListStepHelper + RSpec::Matchers.define :show_todo_item do |item| match do |page| page.has_css? "li", text: item.name end failure_message { %(Expected page to contain a list item "#{item.name}") } end
Implement step to check item has been added The "show_todo_item" matcher expects an item object, so we build (but don't save) one to pass in. Rather than duplicate the name from the earlier step, we extract it to an instance variable. Obviously the scenario now fails because we haven't written the code to add the irem yet, but we can see that it fails for the correct reason.features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "I go to the home page" do visit root_path end When "I add a to-do item" do visit root_path - add_item "Make this test pass" + @new_item_name = "Make this test pass" + add_item @new_item_name end Then "I can see all the items" do expect(page).to show_todo_item @item_1 expect(page).to show_todo_item @item_2 end + +Then "it appears in the list" do + expect(page).to show_todo_item Item.new(name: @new_item_name) +end
Failing spec for creating an itemspec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } before do allow(Item).to receive(:all) { items } end it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end describe "POST create" do + before do + allow(Item).to receive :create + end + + it "creates a new item" do + post :create, item: {name: "do it!"} + expect(Item).to have_received(:create).with name: "do it!" + end + it "redirects to the index" do post :create expect(response).to redirect_to action: :index end end end
Make item creation spec pass Unfortunately in the process we've broken the other spec for the create action, because we aren't passing the required parameter in.app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create + Item.create params.require(:item).permit :name redirect_to action: :index end end
Pass required params in both specs We've pulled the post out into its own method to avoid duplication.spec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } before do allow(Item).to receive(:all) { items } end it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end describe "POST create" do before do allow(Item).to receive :create end - it "creates a new item" do + def do_create post :create, item: {name: "do it!"} + end + + it "creates a new item" do + do_create expect(Item).to have_received(:create).with name: "do it!" end it "redirects to the index" do - post :create + do_create expect(response).to redirect_to action: :index end end end
Everything's green again, so remove the WIP tagfeatures/todo_list.feature
Feature: to-do list Scenario: See what I've got to do Given I have some items in my to-do list When I go to the home page Then I can see all the items - @wip Scenario: Add an item to my to-do list When I add a to-do item Then it appears in the list
Add some basic styling to the page In a real app, I'd probably use bootstrap-rails.app/assets/stylesheets/todo.css.scss
+$background-colour: #acc; + +body { + font-family: Helvetica Neue,Helvetica,Arial,sans-serif; + background-color: $background-colour; + padding: 0; + margin: 0; +} + +.content { + background-color: white; + width: 800px; + height: 100%; + margin: 0 auto; + padding: 20px; +} + +ul { + padding: 0; + border-top: 2px solid $background-colour; + border-bottom: 2px solid $background-colour; + + li { + border-top: 1px solid $background-colour; + font-size: 150%; + list-style: none; + padding: 10px 0; + + &:first-child { + border-top: none; + } + } +}app/views/layouts/application.html.erb
<!DOCTYPE html> <html> <head> <title>To-do list</title> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> </head> <body> - -<%= yield %> - + <div class="content"> + <h1>To-do list</h1> + <%= yield %> + </div> </body> </html>
Own up to rewriting history in the readmeREADME.md
# Sample "to-do" Rails app Created for a demo of TDD with [http://rubyonrails.org/](Rails), using [http://rspec.info/](RSpec) and [https://cukes.info/](Cucumber). See full commit messages for step-by-step description. Where files were changed by running a command rather than manually, that step will be a separate commit, with the command(s) listed in the commit message. +The steps in the history are more-or-less as I actually did them, but a glance +at the commit dates will make it obvious that I've gone back and rebased a +couple of things where I missed something out or changed my mind, in order to +keep a clean narrative. + To keep things simple, we're not using any additional testing tools apart from RSpec and Cucumber (apart from Database Cleaner and Capybara, which Cucumber depends on by default). In a real project, some or all of the following might be useful: * [factory_girl](https://github.com/thoughtbot/factory_girl) for building test data * [faker](https://github.com/stympy/faker) for more realistic test values * [SitePrism](https://github.com/natritmeyer/site_prism) to describe your site using the [page object model](http://martinfowler.com/bliki/PageObject.html) pattern * [timecop](https://github.com/travisjeffery/timecop) to test time-dependent code * [poltergeist](https://github.com/teampoltergeist/poltergeist) to use capybara on pages that use javascript * [capybara-screenshot](https://github.com/mattheworiordan/capybara-screenshot) to automatically open a screenshot of the current page when a test fails Personally, I'd also use [haml](http://haml.info/) instead of erb for the view templating, but we're sticking with the default here for simplicity.
Make the item name into a link to mark it as done This breaks the page because we don't have "mark done" route yet: undefined method `mark_done_item_path' ... Once again, we'll make the small steps on a branch until everything's green again.app/views/items/_item.html.erb
-<li><%= item.name %></li> +<li><%= link_to item.name, mark_done_item_path(item), method: :post %></li>
Add a "mark done" route for items This allows the page to be displayed again, and also moves the cucumber error on slightly: The action 'mark_done' could not be found for ItemsControllerconfig/routes.rb
Rails.application.routes.draw do - resources :items, only: %i(index create) + resources :items, only: %i(index create) do + member do + post :mark_done + end + end root to: "items#index" end
Add some styling to mark-as-done links Maybe getting a bit ahead of ourselves as they don't actually work yet, but let's do it now before we move on.app/assets/stylesheets/todo.css.scss
$background-colour: #acc; body { font-family: Helvetica Neue,Helvetica,Arial,sans-serif; background-color: $background-colour; padding: 0; margin: 0; } .content { background-color: white; width: 800px; height: 100%; margin: 0 auto; padding: 20px; } ul { padding: 0; border-top: 2px solid $background-colour; border-bottom: 2px solid $background-colour; li { border-top: 1px solid $background-colour; font-size: 150%; list-style: none; padding: 10px 0; &:first-child { border-top: none; } + + a { + &:link, &:visited, &:active { + text-decoration: none; + color: black; + } + + &:hover { + text-decoration: line-through; + } + } } }
Add controller action for mark_done Once again, the error changes to a missing view: Missing template items/mark_done, application/mark_done ...app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create Item.create params.require(:item).permit :name redirect_to action: :index end + + def mark_done + end end
Failing spec for mark_done redirecting back to the indexspec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } before do allow(Item).to receive(:all) { items } end it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end describe "POST create" do before do allow(Item).to receive :create end def do_create post :create, item: {name: "do it!"} end it "creates a new item" do do_create expect(Item).to have_received(:create).with name: "do it!" end it "redirects to the index" do do_create expect(response).to redirect_to action: :index end end + + describe "POST mark_done" do + def do_mark_done + post :mark_done, id: "1" + end + + it "redirects to the index" do + do_mark_done + expect(response).to redirect_to action: :index + end + end end
Add WIP scenario for marking an item as donefeatures/todo_list.feature
Feature: to-do list Scenario: See what I've got to do Given I have some items in my to-do list When I go to the home page Then I can see all the items Scenario: Add an item to my to-do list When I add a to-do item Then it appears in the list + + @wip + Scenario: Mark an item as done + Given I have some items in my to-do list + When I click on an item + Then it is marked as done
Make the mark_done action redirect to the index This allows the step to succeed, so now we can move on to actully checking that the item gets marked as done (which obviously it won't yet, because we're working test-first).app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create Item.create params.require(:item).permit :name redirect_to action: :index end def mark_done + redirect_to action: :index end end
Add step definition for clicking on an item Arguably using a helper method here (rather than just putting the "click_on" directly in the step) is overkill, but let's stick to our rule of keeping the knowledge of how to interact with page elements separate.features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "I go to the home page" do visit root_path end When "I add a to-do item" do visit root_path @new_item_name = "Make this test pass" add_item @new_item_name end +When "I click on an item" do + visit root_path + click_item @item_1 +end + Then "I can see all the items" do expect(page).to show_todo_item @item_1 expect(page).to show_todo_item @item_2 end Then "it appears in the list" do expect(page).to show_todo_item Item.new(name: @new_item_name) endfeatures/support/todo_list_step_helper.rb
require "rspec/expectations" module TodoListStepHelper def add_item name fill_in "New item:", with: name click_on "Add" + + end + + def click_item item + click_on item.name end end World TodoListStepHelper RSpec::Matchers.define :show_todo_item do |item| match do |page| page.has_css? "li", text: item.name end failure_message { %(Expected page to contain a list item "#{item.name}") } end
Implement step for checking item marked as done It fails initially because we're using a matcher that we haven't written yet: undefined method `show_item_as_done' ...features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "I go to the home page" do visit root_path end When "I add a to-do item" do visit root_path @new_item_name = "Make this test pass" add_item @new_item_name end When "I click on an item" do visit root_path click_item @item_1 end Then "I can see all the items" do expect(page).to show_todo_item @item_1 expect(page).to show_todo_item @item_2 end Then "it appears in the list" do expect(page).to show_todo_item Item.new(name: @new_item_name) end + +Then "it is marked as done" do + expect(page).to show_item_as_done @item_1 +end
Implement "show item_as_done" matcher Now we get a proper failure message: Expected page to show item "Make a Rails app" as donefeatures/support/todo_list_step_helper.rb
require "rspec/expectations" module TodoListStepHelper def add_item name fill_in "New item:", with: name click_on "Add" end def click_item item click_on item.name end end World TodoListStepHelper RSpec::Matchers.define :show_todo_item do |item| match do |page| page.has_css? "li", text: item.name end failure_message { %(Expected page to contain a list item "#{item.name}") } end + +RSpec::Matchers.define :show_item_as_done do |item| + match do |page| + page.has_css? "li.done", text: item.name + end + + failure_message { %(Expected page to show item "#{item.name}" as done) } +end
Failing spec for mark_done action marking item as done The initial failure message is warning us that we're stubbing a method on the Item class that doesn't exist yet (this is rspec's default behaviour when stubbing or mocking methods on real objects): Item(id: integer, name: string) does not implement: mark_donespec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } before do allow(Item).to receive(:all) { items } end it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end describe "POST create" do before do allow(Item).to receive :create end def do_create post :create, item: {name: "do it!"} end it "creates a new item" do do_create expect(Item).to have_received(:create).with name: "do it!" end it "redirects to the index" do do_create expect(response).to redirect_to action: :index end end describe "POST mark_done" do + before do + allow(Item).to receive :mark_done + end + def do_mark_done post :mark_done, id: "1" end + it "marks the item as done" do + do_mark_done + expect(Item).to have_received(:mark_done).with "1" + end + it "redirects to the index" do do_mark_done expect(response).to redirect_to action: :index end end end
Add empty Item.mark_done method This allows the controller spec to continue, and it now fails as expected, because we aren't calling the method yet: Failure/Error: expect(Item).to have_received(:mark_done).with "1" (<Item(id: integer, name: string) (class)>).mark_done("1") expected: 1 time with arguments: ("1") received: 0 timesapp/models/item.rb
class Item < ActiveRecord::Base + def self.mark_done + end end
Call Item.mark_done from the controller We expect this to pass, but in fact it fails: Wrong number of arguments. Expected 0, got 1. This is because we didn't specify any arguments when we created the empty implementation of Item.mark_done.app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create Item.create params.require(:item).permit :name redirect_to action: :index end def mark_done + Item.mark_done params[:id] redirect_to action: :index end end
Add id argument to Item.mark_done This allows the controller spec to pass, but the feature still fails, because we need to actually do something in the mark_done method on the model.app/models/item.rb
class Item < ActiveRecord::Base - def self.mark_done + def self.mark_done id end end
Add a spec for Item.mark_done This is a test for ActiveRecord code, so we integrate with the database by creating an item, then calling the method, and finally reloading the item and checking that it's been updated appropriately (as opposed to in controller specs, where we use test doubles to check interactions with the model). Initially it fails because the Item class doesn't currently have any concept of "done": expected #<Item:0x007feafe3bf8f8> to respond to `done?`spec/models/item_spec.rb
+require "rails_helper" + +describe Item do + describe ".mark_done" do + it "marks the specified item as done" do + item = Item.create name: "An item" + Item.mark_done item.id + expect(item.reload).to be_done + end + end +end
Add a "done" column to items bin/rails g migration AddDoneToItem done:boolean rake db:migrate Now we've got the column, our spec fails as expected, indicating that we haven't updated the item to set done to true: expected `#<Item id: 1, name: "An item", done: nil>.done?` to return true, got falsedb/migrate/20150322113241_add_done_to_item.rb
+class AddDoneToItem < ActiveRecord::Migration + def change + add_column :items, :done, :boolean + end +enddb/schema.rb
# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # Note that this schema.rb definition is the authoritative source for your # database schema. If you need to create the application database on another # system, you should be using db:schema:load, not running all the migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150321115919) do +ActiveRecord::Schema.define(version: 20150322113241) do create_table "items", force: :cascade do |t| - t.string "name" + t.string "name" + t.boolean "done" end end
Implement Item.mark_done method The specs all pass now, but our feature is still failing. Although we are marking the item as done, we haven't updated the view to show the difference between done and to-do items.app/models/item.rb
class Item < ActiveRecord::Base def self.mark_done id + update id, done: true end end
Show done items appropriately in view This makes all the tests pass, but we aren't quite done. Looking at the site in the browser we realise that although we've added the "done" class, we haven't written any CSS rules yet to display done items differently. This is a good reminder to actually look at the app now and again, rather than just relying on tests!app/views/items/_item.html.erb
-<li><%= link_to item.name, mark_done_item_path(item), method: :post %></li> +<% if item.done? %> + <li class="done"><%= item.name %></li> +<% else %> + <li><%= link_to item.name, mark_done_item_path(item), method: :post %></li> +<% end %>
Style done itemsapp/assets/stylesheets/todo.css.scss
$background-colour: #acc; +$done-colour: #888; body { font-family: Helvetica Neue,Helvetica,Arial,sans-serif; background-color: $background-colour; padding: 0; margin: 0; } .content { background-color: white; width: 800px; height: 100%; margin: 0 auto; padding: 20px; } ul { padding: 0; border-top: 2px solid $background-colour; border-bottom: 2px solid $background-colour; li { border-top: 1px solid $background-colour; font-size: 150%; list-style: none; padding: 10px 0; &:first-child { border-top: none; } + &.done { + color: $done-colour; + text-decoration: line-through; + } + a { &:link, &:visited, &:active { text-decoration: none; color: black; } &:hover { text-decoration: line-through; } } } }
Everything's green again, so remove the WIP tagfeatures/todo_list.feature
Feature: to-do list Scenario: See what I've got to do Given I have some items in my to-do list When I go to the home page Then I can see all the items Scenario: Add an item to my to-do list When I add a to-do item Then it appears in the list - @wip Scenario: Mark an item as done Given I have some items in my to-do list When I click on an item Then it is marked as done
Add running instructions to readmeREADME.md
# Sample "to-do" Rails app Created for a demo of TDD with [http://rubyonrails.org/](Rails), using [http://rspec.info/](RSpec) and [https://cukes.info/](Cucumber). +To run the app: + + rake db:migrate + bin/rails s + +Then go to [localhost:3000](http://localhost:3000) in your browser. + +To run the tests: + + rake + See full commit messages for step-by-step description. Where files were changed by running a command rather than manually, that step will be a separate commit, with the command(s) listed in the commit message. +To check out a specific version of the code: + + git log + +Find the SHA of the commit you want to check out, then: + + git checkout <SHA> + rake db:reset + The steps in the history are more-or-less as I actually did them, but a glance at the commit dates will make it obvious that I've gone back and rebased a couple of things where I missed something out or changed my mind, in order to keep a clean narrative. To keep things simple, we're not using any additional testing tools apart from RSpec and Cucumber (apart from Database Cleaner and Capybara, which Cucumber depends on by default). In a real project, some or all of the following might be useful: * [factory_girl](https://github.com/thoughtbot/factory_girl) for building test data * [faker](https://github.com/stympy/faker) for more realistic test values * [SitePrism](https://github.com/natritmeyer/site_prism) to describe your site using the [page object model](http://martinfowler.com/bliki/PageObject.html) pattern * [timecop](https://github.com/travisjeffery/timecop) to test time-dependent code * [poltergeist](https://github.com/teampoltergeist/poltergeist) to use capybara on pages that use javascript * [capybara-screenshot](https://github.com/mattheworiordan/capybara-screenshot) to automatically open a screenshot of the current page when a test fails Personally, I'd also use [haml](http://haml.info/) instead of erb for the view templating, but we're sticking with the default here for simplicity.
Add apology for limited functionality to readmeREADME.md
# Sample "to-do" Rails app Created for a demo of TDD with [http://rubyonrails.org/](Rails), using [http://rspec.info/](RSpec) and [https://cukes.info/](Cucumber). +This is an extremely basic to-do app. It currently allows creation of new items +and marking of items as done, but nothing else. There is also no input +validation, error-checking etc. + To run the app: rake db:migrate bin/rails s Then go to [localhost:3000](http://localhost:3000) in your browser. To run the tests: rake See full commit messages for step-by-step description. Where files were changed by running a command rather than manually, that step will be a separate commit, with the command(s) listed in the commit message. To check out a specific version of the code: git log Find the SHA of the commit you want to check out, then: git checkout <SHA> rake db:reset The steps in the history are more-or-less as I actually did them, but a glance at the commit dates will make it obvious that I've gone back and rebased a couple of things where I missed something out or changed my mind, in order to keep a clean narrative. To keep things simple, we're not using any additional testing tools apart from RSpec and Cucumber (apart from Database Cleaner and Capybara, which Cucumber depends on by default). In a real project, some or all of the following might be useful: * [factory_girl](https://github.com/thoughtbot/factory_girl) for building test data * [faker](https://github.com/stympy/faker) for more realistic test values * [SitePrism](https://github.com/natritmeyer/site_prism) to describe your site using the [page object model](http://martinfowler.com/bliki/PageObject.html) pattern * [timecop](https://github.com/travisjeffery/timecop) to test time-dependent code * [poltergeist](https://github.com/teampoltergeist/poltergeist) to use capybara on pages that use javascript * [capybara-screenshot](https://github.com/mattheworiordan/capybara-screenshot) to automatically open a screenshot of the current page when a test fails Personally, I'd also use [haml](http://haml.info/) instead of erb for the view templating, but we're sticking with the default here for simplicity.
Replace logic in view with two separate partials Logic in views is generally a bad thing, as views can be hard to test in isolation. We're going to replace the if-else logic in the item partial with two separate partials, one for items which are done and another for those which aren't. Obviously this doesn't work yet, as there's no way for Rails to know which partial to render. We'll fix that next.app/views/items/_done_item.html.erb
+<li class="done"><%= done_item.name %></li>app/views/items/_item.html.erb
-<% if item.done? %> - <li class="done"><%= item.name %></li> -<% else %> - <li><%= link_to item.name, mark_done_item_path(item), method: :post %></li> -<% end %>app/views/items/_to_do_item.html.erb
+<li><%= link_to to_do_item.name, mark_done_item_path(to_do_item), method: :post %></li>
Add failing spec for Item#to_partial_path Rails uses the 'to_display_path' method on model instances in collections to decide which partial should be used to display them. In a more complicated model we might decide to use decorator classes to wrap the model instances, to separate business logic from presentation, but for simplicity we'll implement the method directly on the model. The method simply needs to return the right partial name depending on the value of 'done', to match the pair of partials we created earlier.spec/models/item_spec.rb
require "rails_helper" describe Item do + describe "#to_partial_path" do + it "returns 'to_do_item' for items which are not done" do + item = Item.new done: false + expect(item.to_partial_path).to eq "to_do_item" + end + + it "returns 'done_item' for items which are done" do + item = Item.new done: true + expect(item.to_partial_path).to eq "done_item" + end + end + describe ".mark_done" do it "marks the specified item as done" do item = Item.create name: "An item" Item.mark_done item.id expect(item.reload).to be_done end end end
Implement Item#to_partial_pathapp/models/item.rb
class Item < ActiveRecord::Base def self.mark_done id update id, done: true end + + def to_partial_path + done? ? "done_item" : "to_do_item" + end end
Add WIP feature for deleting done itemsfeatures/todo_list.feature
Feature: to-do list Scenario: See what I've got to do Given I have some items in my to-do list When I go to the home page Then I can see all the items Scenario: Add an item to my to-do list When I add a to-do item Then it appears in the list Scenario: Mark an item as done Given I have some items in my to-do list When I click on an item Then it is marked as done + + @wip + Scenario: Remove done items + Given I have some items in my to-do list + And some of the items are done + When I remove done items + Then only the to-do items are left behind
Implement 'Given some of the items are done' stepfeatures/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end +When "some of the items are done" do + Item.mark_done @item_1.id +end + When "I go to the home page" do visit root_path end When "I add a to-do item" do visit root_path @new_item_name = "Make this test pass" add_item @new_item_name end When "I click on an item" do visit root_path click_item @item_1 end Then "I can see all the items" do expect(page).to show_todo_item @item_1 expect(page).to show_todo_item @item_2 end Then "it appears in the list" do expect(page).to show_todo_item Item.new(name: @new_item_name) end Then "it is marked as done" do expect(page).to show_item_as_done @item_1 end
Add step definition for deleting done items We delegate the actual button-pushing to a non-existent helper method, so unsurprisingly it fails: undefined local variable or method `remove_done_items' ...features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "some of the items are done" do Item.mark_done @item_1.id end When "I go to the home page" do visit root_path end When "I add a to-do item" do visit root_path @new_item_name = "Make this test pass" add_item @new_item_name end When "I click on an item" do visit root_path click_item @item_1 end +When "I remove done items" do + visit root_path + remove_done_items +end + Then "I can see all the items" do expect(page).to show_todo_item @item_1 expect(page).to show_todo_item @item_2 end Then "it appears in the list" do expect(page).to show_todo_item Item.new(name: @new_item_name) end Then "it is marked as done" do expect(page).to show_item_as_done @item_1 end
Implement step helper method Now it fails properly: Unable to find link or button "Remove done" ...features/support/todo_list_step_helper.rb
require "rspec/expectations" module TodoListStepHelper def add_item name fill_in "New item:", with: name click_on "Add" end def click_item item click_on item.name end + + def remove_done_items + click_on "Remove done" + end end World TodoListStepHelper RSpec::Matchers.define :show_todo_item do |item| match do |page| page.has_css? "li", text: item.name end failure_message { %(Expected page to contain a list item "#{item.name}") } end RSpec::Matchers.define :show_item_as_done do |item| match do |page| page.has_css? "li.done", text: item.name end failure_message { %(Expected page to show item "#{item.name}" as done) } end
Add 'Remove done' button We've specified a non-existent route, so the scenario now clicks the button but gets an error: undefined local variable or method `remove_done_items_path' ... At this point we've broken the app again, so we wouldn't normally commit (at least on master) until we had things green again.app/views/items/index.html.erb
<ul> <%= render @items %> </ul> +<p><%= button_to "Remove done", remove_done_items_path %></p> <%= form_for Item.new do |f| %> <%= f.label :name, "New item:" %> <%= f.text_field :name %> <%= f.submit "Add" %> <% end %>
Add route for remove_done on items collection We now get a new error: The action 'remove_done' could not be found for ItemsControllerconfig/routes.rb
Rails.application.routes.draw do resources :items, only: %i(index create) do member do post :mark_done end + collection do + post :remove_done + end end root to: "items#index" end
Implement remove_done action As before, we now get an error because by default Rails tries to render a template matching the action, and there isn't one: Missing template items/remove_done, application/remove_done ...app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create Item.create params.require(:item).permit :name redirect_to action: :index end def mark_done Item.mark_done params[:id] redirect_to action: :index end + + def remove_done + end end
Add a failing spec for redirect after remove_done The spec initially fails with the same missing template error as the cucumber scenario.spec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } before do allow(Item).to receive(:all) { items } end it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end describe "POST create" do before do allow(Item).to receive :create end def do_create post :create, item: {name: "do it!"} end it "creates a new item" do do_create expect(Item).to have_received(:create).with name: "do it!" end it "redirects to the index" do do_create expect(response).to redirect_to action: :index end end describe "POST mark_done" do before do allow(Item).to receive :mark_done end def do_mark_done post :mark_done, id: "1" end it "marks the item as done" do do_mark_done expect(Item).to have_received(:mark_done).with "1" end it "redirects to the index" do do_mark_done expect(response).to redirect_to action: :index end end + + describe "POST remove_done" do + def do_remove_done + post :remove_done + end + + it "redirects to the index" do + do_remove_done + expect(response).to redirect_to action: :index + end + end end
Make the redirect spec pass This now allow the cucumber step to complete, so we're left with just one unimplemented step definition.app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create Item.create params.require(:item).permit :name redirect_to action: :index end def mark_done Item.mark_done params[:id] redirect_to action: :index end def remove_done + redirect_to action: :index end end
Implement step to check done item has been removed We're reusing an existing matcher, so the test fails with the expected error (since we aren't deleting anything yet): expected #<Capybara::Session> not to show todo item #<Item id: 1 ...features/step_definitions/todo_list_steps.rb
Given "I have some items in my to-do list" do @item_1 = Item.create name: "Make a Rails app" @item_2 = Item.create name: "Demonstrate test-driven development" end When "some of the items are done" do Item.mark_done @item_1.id end When "I go to the home page" do visit root_path end When "I add a to-do item" do visit root_path @new_item_name = "Make this test pass" add_item @new_item_name end When "I click on an item" do visit root_path click_item @item_1 end When "I remove done items" do visit root_path remove_done_items end Then "I can see all the items" do expect(page).to show_todo_item @item_1 expect(page).to show_todo_item @item_2 end Then "it appears in the list" do expect(page).to show_todo_item Item.new(name: @new_item_name) end Then "it is marked as done" do expect(page).to show_item_as_done @item_1 end + +Then "only the to-do items are left behind" do + expect(page).not_to show_todo_item @item_1 + expect(page).to show_todo_item @item_2 +end
Add failing spec for controller calling Item.remove_done This method doesn't esist yet, and as we're mocking it on a real object, rspec warns us: Item ... does not implement: remove_donespec/controllers/items_controller_spec.rb
require "rails_helper" describe ItemsController do describe "GET index" do let(:items) { [double(:item)] } before do allow(Item).to receive(:all) { items } end it "assigns all the items for the view" do get :index expect(assigns[:items]).to eq items end end describe "POST create" do before do allow(Item).to receive :create end def do_create post :create, item: {name: "do it!"} end it "creates a new item" do do_create expect(Item).to have_received(:create).with name: "do it!" end it "redirects to the index" do do_create expect(response).to redirect_to action: :index end end describe "POST mark_done" do before do allow(Item).to receive :mark_done end def do_mark_done post :mark_done, id: "1" end it "marks the item as done" do do_mark_done expect(Item).to have_received(:mark_done).with "1" end it "redirects to the index" do do_mark_done expect(response).to redirect_to action: :index end end describe "POST remove_done" do + before do + allow(Item).to receive :remove_done + end + def do_remove_done post :remove_done end + it "removes done items" do + do_remove_done + expect(Item).to have_received(:remove_done) + end + it "redirects to the index" do do_remove_done expect(response).to redirect_to action: :index end end end
Add an empty remove_done class method to Item The controller spec now fails as expected, because we're not calling the method: Failure/Error: expect(Item).to have_received(:remove_done)app/models/item.rb
class Item < ActiveRecord::Base def self.mark_done id update id, done: true end + def self.remove_done + end + def to_partial_path done? ? "done_item" : "to_do_item" end end
Call Item.remove_done from controller This makes the controller spec pass, but the feature still fails, because the model method doesn't do anything yet.app/controllers/items_controller.rb
class ItemsController < ApplicationController def index @items = Item.all end def create Item.create params.require(:item).permit :name redirect_to action: :index end def mark_done Item.mark_done params[:id] redirect_to action: :index end def remove_done + Item.remove_done redirect_to action: :index end end
Add failing spec for Item.remove_done This spec is testing the model's integration with the database, so we create and save Item objects, and check what's in the database after calling the method. It fails because the done item isn't removed: Failure/Error: expect(Item.all).to eq [to_do_item] expected: [#<Item id: 1, name: "To-do item", done: nil>] got: #<ActiveRecord::Relation [#<Item id: 1, name: "To-do item", done: nil>, #<Item id: 2, name: "Done item", done: true>]>spec/models/item_spec.rb
require "rails_helper" describe Item do describe "#to_partial_path" do it "returns 'to_do_item' for items which are not done" do item = Item.new done: false expect(item.to_partial_path).to eq "to_do_item" end it "returns 'done_item' for items which are done" do item = Item.new done: true expect(item.to_partial_path).to eq "done_item" end end describe ".mark_done" do it "marks the specified item as done" do item = Item.create name: "An item" Item.mark_done item.id expect(item.reload).to be_done end end + + describe ".remove_done" do + it "removes all items which are marked as done" do + to_do_item = Item.create name: "To-do item" + Item.create name: "Done item", done: true + Item.remove_done + expect(Item.all).to eq [to_do_item] + end + end end
Improve spec failure message The previous failure message wasn't particularly readable, so we assert against the item names, rather than the objects themselves. This means we don't need to assign the item we expect to find to a variable, and gives a much clearer failure message: expected: ["To-do item"] got: ["To-do item", "Done item"]spec/models/item_spec.rb
require "rails_helper" describe Item do describe "#to_partial_path" do it "returns 'to_do_item' for items which are not done" do item = Item.new done: false expect(item.to_partial_path).to eq "to_do_item" end it "returns 'done_item' for items which are done" do item = Item.new done: true expect(item.to_partial_path).to eq "done_item" end end describe ".mark_done" do it "marks the specified item as done" do item = Item.create name: "An item" Item.mark_done item.id expect(item.reload).to be_done end end describe ".remove_done" do it "removes all items which are marked as done" do - to_do_item = Item.create name: "To-do item" + Item.create name: "To-do item" Item.create name: "Done item", done: true Item.remove_done - expect(Item.all).to eq [to_do_item] + expect(Item.all.map(&:name)).to eq ["To-do item"] end end end
Implement Item.remove_done This passes both the spec and the feature, so we also remove the WIP tag.app/models/item.rb
class Item < ActiveRecord::Base def self.mark_done id update id, done: true end def self.remove_done + destroy_all done: true end def to_partial_path done? ? "done_item" : "to_do_item" end endfeatures/todo_list.feature
Feature: to-do list Scenario: See what I've got to do Given I have some items in my to-do list When I go to the home page Then I can see all the items Scenario: Add an item to my to-do list When I add a to-do item Then it appears in the list Scenario: Mark an item as done Given I have some items in my to-do list When I click on an item Then it is marked as done - @wip Scenario: Remove done items Given I have some items in my to-do list And some of the items are done When I remove done items Then only the to-do items are left behind
One reply on “TDD example with Rails, Cucumber and RSpec”
[…] It runs through the git history of a project, printing the full message for each commit, followed by a complete listing of each changed file, with added, removed or changed lines highlighted. It then outputs the result into a big ugly HTML file. Like this. […]