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 .md
README.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 readme
README.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 Gemfile
Gemfile
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 use
Gemfile
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'
end
Gemfile.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 gem
Gemfile
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
Gemfile.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_helper
spec/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!
+end
spec/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 gems
Gemfile
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"
+end
Gemfile.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:install
config/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 ~@wip
config/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 file
features/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
+
+end
script/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:
bundle
features/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 wip
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
+
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
+end
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
+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 readme
README.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 layout
app/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 list
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
+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: nil
spec/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
end
features/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
end
features/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 item
features/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 item
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
+ 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 tag
features/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 readme
README.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 ItemsController
config/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 index
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
+ 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 done
features/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)
end
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
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 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
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_done
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
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 times
app/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 false
db/migrate/20150322113241_add_done_to_item.rb
+class AddDoneToItem < ActiveRecord::Migration
+ def change
+ add_column :items, :done, :boolean
+ end
+end
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: 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 items
app/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 tag
features/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 readme
README.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 readme
README.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_path
app/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 items
features/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' step
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
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 ItemsController
config/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_done
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
+ 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
end
features/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. […]