We're asked to build an application that allows a user to submit inquiries. The user stories come in two parts. In the first part, we create a simple form with text inputs to capture an inquiry. In the second part, we're asked to categorize the inquiries that are submitted.
We will build to the first story and then turn our attention to the second once our tests are passing.
As a site visitor
I want to contact the site's staff
So that I can tell them how awesome they are
Acceptance Criteria:
- I must specify a valid email address
- I must specify a subject
- I must specify a description
New requirements come in from our stakeholders
As a site visitor
I want to contact the site's staff
So that I can tell them how awesome they are
Acceptance Criteria:
- I must specify a valid email address
- I must specify a subject
- I must specify a description
- I must specify a category from a predefined list (Praise, Complaint)
Follow the steps below to get the application set up to the point where we can start creating acceptance tests.
rails new inquiries -T -d postgresql
git init && git add . && git commit -m "First commit"
rvm --ruby-version use 2.0.0@inquiries --create
git add . && git commit -m "Add ruby version and gemset"
Resources:
group :development, :test do
gem 'rspec-rails'
gem 'capybara'
gem 'valid_attribute'
gem 'shoulda-matchers'
gem 'factory_girl_rails'
gem 'launchy'
gem 'quiet_assets'
gem 'pry-rails'
end
bundle install
git add . && git commit -m "Add testing gems"
Add config/database.yml to .gitignore
echo "config/database.yml" >> .gitignore
cp config/database.yml config/database.example.yml
Modify config/database.example.yml
and config/database.yml
as needed.
Commit:
git add . && git commit -m "Ignore default database
configuration and provide example instead"
rake db:create && rake db:migrate
Commit:
git add . && git commit -m "Setup database and add schema"
rails g rspec:install
Verify install:
rake spec
Commit:
git add . && git commit -m "Install RSpec"
You're now ready for first User Story.
Create a feature branch
git checkout -b user-submits-inquiry
Create first feature spec
In spec/features/user_submits_inquiry_spec.rb
**:
require 'spec_helper'
feature 'user submits inquiry', %Q{
As a site visitor
I want to contact the site's staff
So that I can tell them how awesome they are
} do
# Acceptance Criteria:
# I must specify a valid email address
# I must specify a subject
# I must specify a description
scenario 'create inquiry with valid attributes'
scenario 'fail to create inquiry and show errors with invalid attributes'
end
Run RSpec
rspec spec/features/user_submits_inquiry_spec.rb
You should get the two pending specs.
Modify you first scenario as shown below:
scenario 'create inquiry with valid attributes' do
visit '/inquiries/new'
fill_in 'Email', with: 'user@example.com'
fill_in 'Subject', with: 'You rock!'
fill_in 'Description', with: 'I really like your site!'
click_button 'Submit'
expect(page).to have_content 'user@example.com'
expect(page).to have_content 'You rock!'
expect(page).to have_content 'I really like your site!'
end
Run rspec
and optionally specify the line number of the spec you want to focus on to limit the amount of noise on the console:
rspec spec/features/user_submits_inquiry_spec.rb:16
You should get a RoutingError
:
create inquiry with valid attributes
Failure/Error: visit '/inquiries/new'
ActionController::RoutingError:
No route matches [GET] "/inquiries/new"
# ./spec/features/user_submits_inquiry_spec.rb:17:in `block (2 levels) in <top (required)>'
Add resourceful routes for inquiries
Edit config/routes.rb
and add resources :inquiries
.
Verify that you have a new set of routes with
rake routes
Rerun your specs and you should see:
create inquiry with valid attributes
Failure/Error: visit '/inquiries/new'
ActionController::RoutingError:
uninitialized constant InquiriesController
# ./spec/features/user_submits_inquiry_spec.rb:17:in `block (2 levels) in <top (required)>'
Create the InquiriesController
rails g controller inquiries
Remove the extraneous
spec/controllers
andspec/helpers
directories.
Run your specs and get:
create inquiry with valid attributes
Failure/Error: visit '/inquiries/new'
AbstractController::ActionNotFound:
The action 'new' could not be found for InquiriesController
# ./spec/features/user_submits_inquiry_spec.rb:17:in `block (2 levels) in <top (required)>'
Add the new
action to our controller
class InquiriesController < ApplicationController
def new
end
end
The
new
action is light on purpose. Remember, we just do the minimum that will make the test pass.
Run our specs and get an error about missing a template:
create inquiry with valid attributes
Failure/Error: visit '/inquiries/new'
ActionView::MissingTemplate:
Missing template inquiries/new
...
Create app/views/inquiries/new.html.erb
template and leave it empty.
Run your specs and get:
create inquiry with valid attributes
Failure/Error: fill_in 'Email', with: 'user@example.com'
Capybara::ElementNotFound:
Unable to find field "Email"
Now we're getting somewhere
We need to add our form details to app/views/inquiries/new.html.erb
at this point:
<h1>New Inquiry</h1>
<%= form_for(@inquiry) do |f| %>
<div>
<%= f.label :email %>
<%= f.text_field :email %>
</div>
<div>
<%= f.label :subject %>
<%= f.text_field :subject %>
</div>
<div>
<%= f.label :description %>
<%= f.text_field :description %>
</div>
<div>
<%= f.submit 'Submit' %>
</div>
<% end %>
Run the specs and get:
create inquiry with valid attributes
Failure/Error: visit '/inquiries/new'
ActionView::Template::Error:
First argument in form cannot contain nil or be empty
# ./app/views/inquiries/new.html.erb:2
...
The @inquiry instance is nil. Let's fix that.
In application/controllers/inquiries_controller.rb
, return a new Inquiry
instance from the new
method:
def new
@inquiry = Inquiry.new
end
Run your specs and get:
create inquiry with valid attributes
Failure/Error: visit '/inquiries/new'
NameError:
uninitialized constant InquiriesController::Inquiry
# ./app/controllers/inquiries_controller.rb:4:in `new'
We do not yet have an
Inquiry
model. Let's generate one.
Generate an Inquiry
model
rails g model inquiry email subject description:text
Attempting to run our specs will yield a migration error. Before we do a migration however, we must let our user story dictate our model constraints which in turn dictate our database constraints.
Add validations to Inquiry
model spec
In spec/models/inquiry_spec.rb
:
describe Inquiry do
let(:blanks){ [nil, ''] }
it { should have_valid(:email).when('user@example.com', 'jdoe@domain.com') }
it { should_not have_valid(:email).when('@domain.com', 'jdoe') }
it { should_not have_valid(:email).when(*blanks) }
it { should have_valid(:subject).when('A subject') }
it { should_not have_valid(:subject).when(*blanks)}
it { should have_valid(:description).when('A description') }
it { should_not have_valid(:description).when(*blanks)}
end
Now that we have our validations on the model, let's ensure the migration reflects those constraints.
Modify the xxx_create_inquiries
migration in db/migrate
to have null constraints set for the fields:
class CreateInquiries < ActiveRecord::Migration
def change
create_table :inquiries do |t|
t.string :email, null: false
t.string :subject, null: false
t.text :description, null: false
t.timestamps
end
end
end
Run our migration
rake db:migrate && rake db:rollback && rake db:migrate && rake db:test:prepare
Run your inquiry model spec:
rspec spec/models/inquiry_spec.rb
Fix the validation-related errors by adding the necessary validations to the Inquiry
model:
class Inquiry < ActiveRecord::Base
validates :email, format: { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i }
validates_presence_of :subject
validates_presence_of :description
end
Note the format validation on email which enforces that only values in the correct format can be persisted.
Run our model specs again and we should be green:
.......
Finished in 0.03346 seconds
7 examples, 0 failures
We're now ready to get back to our acceptance test. Run the test for spec/features/user_submits_inquiry_spec.rb
:
create inquiry with valid attributes
Failure/Error: click_button 'Submit'
AbstractController::ActionNotFound:
The action 'create' could not be found for InquiriesController
We're now faced with a missing controller action. Let's add it.
Add an empty create
method to InquiriesController
and rerun the specs:
create inquiry with valid attributes
Failure/Error: click_button 'Submit'
ActionView::MissingTemplate:
Missing template inquiries/create...
Implement the create
method in InquiriesController
which should look something like this:
class InquiriesController < ApplicationController
def new
@inquiry = Inquiry.new
end
def create
@inquiry = Inquiry.new(inquiry_params)
if @inquiry.save
redirect_to @inquiry, notice: 'Inquiry successfully submitted'
else
render 'new'
end
end
protected
def inquiry_params
params.require(:inquiry).permit([:email, :subject, :description])
end
end
Note the protected
inquiry_params
method which whitelists the form attributes that are allowed to come through and be available for persistence.
Also note that after successful creation, we redirect to the inquiry
show
page and provide a flash notice to be shown on that page. Should the create fail, we send the client back to thenew
page.
The error we get is a bit misleading at first but it just means that a show
template is being looked for following the call to create
.
Create an empty app/views/inquiries/show.html.erb
and run the specs again:
Let's run our feature specs again and this time we get:
create inquiry with valid attributes
Failure/Error: expect(page).to have_content 'user@example.com'
expected to find text "user@example.com" in ""
# ./spec/features/user_submits_inquiry_spec.rb
This tells us that although we able to successfully submit the form, our show
page does not meet our expectations. Let's fix that.
Edit app/views/inquiries/show.html.erb
to reflex the following:
<h1>Inquiry</h1>
<p><%= notice %></p>
<p>Email: <%= @inquiry.email %></p>
<p>Subject: <%= @inquiry.subject %></p>
<p>Description: <%= @inquiry.description %></p>
Running our specs will yield an error:
create inquiry with valid attributes
Failure/Error: click_button 'Submit'
ActionView::Template::Error:
undefined method `email' for nil:NilClass
# ./app/views/inquiries/show.html.erb:3
This error points out the fact that our @inquiry
object is still nil
. Since the show
page relies on values set in the show
action of our InquiriesController
, let's ensure that we load and make available the Inquiry
in question.
Implement the show
action in app/controllers/inquiry_controller.rb
:
def show
@inquiry = Inquiry.find(params[:id])
end
Finally, when we run our feature spec once more, it passes:
.
Finished in 0.13908 seconds
1 example, 0 failures
Testing to the not-so happy path
Return to our spec/features/user_submits_inquiry_spec.rb
and implement the scenario that checks for failures:
scenario 'fail to create inquiry and show errors with invalid attributes' do
visit '/inquiries/new'
click_button 'Submit'
expect(page).to have_content("Email is invalid")
expect(page).to have_content("Subject can't be blank")
expect(page).to have_content("Description can't be blank")
end
As expected, our test fails:
fail to create inquiry and show errors with invalid attributes
Failure/Error: expect(page).to have_content("Email is invalid")
expected to find text "Email is invalid" in "New Inquiry Email Subject Description"
# ./spec/features/user_submits_inquiry_spec.rb:32
The reason for the expected failure is that when the create doesn't succeed and the new
template is rendered (as per the create
action of our InquiriesController
), we're not displaying the validation errors anywhere on the page and as a result, our capybara can't find the content we expect in the page.
Let's fix that by adding the following to our app/views/inquiries/new.html.erb
template:
<% if @inquiry.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@inquiry.errors.count, "error") %> prohibited this inquiry from being saved:</h2>
<ul>
<% @inquiry.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
We borrowed this code from one of our previous scaffolds because it does a good enough job of iterating over the validation errors on a model and outputting said errors in a list.
All of our tests should now be passing:
.........
Finished in 0.22286 seconds
9 examples, 0 failures
Commit and merge with master
git add -A . && git commit -m "Add inquiry submission"
git checkout master && git merge user-submits-inquiry
We're now at the point where we can introduce the category association into our application. Recall that our stakeholders would like us to capture the category of inquiry submission from our users.
If we compare the two sets of Acceptance Criteria from the start of this walkthrough, we'll notice that the only difference is
* I must specify a category from a predefined list (Praise, Complaint)
This tells us that we can safely augment our existing feature spec to look for this new information.
Before we proceed, let's do some housekeeping.
Create a feature branch
git checkout -b user-submits-inquiry-with-category
Change our feature spec
In spec/features/user_submits_inquiry_spec.rb
:
- add the new acceptance criteria to our list
- use
select
to pick an option from an expected drop down select - add a new expectation for the selected option to the page
After our modifications, our scenario should now look like this:
feature 'user submits inquiry', %Q{
As a site visitor
I want to contact the site's staff
So that I can tell them how awesome they are
} do
# Acceptance Criteria:
# I must specify a valid email address
# I must specify a subject
# I must specify a description
# I must specify a category from a predefined list (Praise, Complaint)
scenario 'create inquiry with valid attributes' do
visit '/inquiries/new'
fill_in 'Email', with: 'user@example.com'
fill_in 'Subject', with: 'You rock!'
fill_in 'Description', with: 'I really like your site!'
select 'Praise', from: 'Category'
click_button 'Submit'
expect(page).to have_content 'user@example.com'
expect(page).to have_content 'You rock!'
expect(page).to have_content 'I really like your site!'
expect(page).to have_content 'Praise'
end
Run our specs and get:
create inquiry with valid attributes
Failure/Error: select 'Praise', from: 'Category'
Capybara::ElementNotFound:
Unable to find select box "Category"
# ./spec/features/user_submits_inquiry_spec.rb:22
Sure enough, we're expecting a drop down field on the page that isn't there. Let's fix that.
Update the new
template
Modify app/views/in quiries/new.html.rb
to add a collection_select
helper that will expect its values from a Category
model.
<div>
<%= f.label :category_id %>
<%= f.collection_select :category_id, Category.all, :id, :name, include_blank: true %>
</div>
A run of our test indicates the lack of a Category
model. Before generate it however, we know that there's an association between and Inquiry
and a Category
. Mainly that Inquiry
belongs to Category
and that Category
has many 'Issue's. We already have an inquiry spec so let's add a check for this association to move our test forward.
**Add association check for Categiry
in Inquiry
model **
In spec/models/inquiry_spec.rb
add the following validation:
it { should belong_to :category }
Run the model specs:
1) Inquiry should belong to category
Failure/Error: it { should belong_to :category }
Expected Inquiry to have a belongs_to association called category (no association called category)
Add the association to our Inquiry model:
class Inquiry < ActiveRecord::Base
belongs_to :category, inverse_of: :inquiries
validates :email, format: { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i }
validates_presence_of :subject
validates_presence_of :description
end
Run the model specs:
1) Inquiry should belong to category
Failure/Error: it { should belong_to :category }
Expected Inquiry to have a belongs_to association called category (Inquiry does not have a category_id foreign key.)
Let's create a migration to add the category_id
column:
rails g migration AddCategoryIdToInquiries category_id:integer
Perform our migration:
rake db:migrate && rake db:rollback && rake db:migrate && rake db:test:prepare
Run our specs again to go green.
Still missing Category
Back in our feature specs, we're still failure because we're expecting the presence of a Category
model.
Let's now introduce this model by generating it:
rails g model Category name
Set up validation and association tests, alter our migration to reflect our validation constraints and run our tests.
spec/models/category_spec.rb
require 'spec_helper'
describe Category do
it { should have_valid(:name).when('A category') }
it { should_not have_valid(:name).when(nil, '')}
it { should have_many(:inquiries) }
end
app/models/category.rb
class Category < ActiveRecord::Base
has_many :inquiries, inverse_of: :category
validates_presence_of :name
end
All tests should be green after preparing the
Category
model.
...........
Finished in 0.06486 seconds
11 examples, 0 failures
Back to our acceptance tests
Back in our acceptance tests, we should now be seeing a different type of failure:
create inquiry with valid attributes
Failure/Error: select 'Praise', from: 'Category'
Capybara::ElementNotFound:
Unable to find option "Praise"
# ./spec/features/user_submits_inquiry_spec.rb:22
The issue there is really around the lack of the options we're expecting in the rendered form.
We can fix that by creating those a sample set of categories to be rendered prior to using capybara to look for them.
Modify our "happy path" scenario to look like this:
scenario 'create inquiry with valid attributes' do
Category.create!(name: 'Praise')
Category.create!(name: 'Complaint')
visit '/inquiries/new'
fill_in 'Email', with: 'user@example.com'
fill_in 'Subject', with: 'You rock!'
fill_in 'Description', with: 'I really like your site!'
select 'Praise', from: 'Category'
click_button 'Submit'
expect(page).to have_content 'user@example.com'
expect(page).to have_content 'You rock!'
expect(page).to have_content 'I really like your site!'
expect(page).to have_content 'Praise'
end
Run your feature spec and instead of success, we end up with a somewhat unexpected error:
reate inquiry with valid attributes
Failure/Error: expect(page).to have_content 'Praise'
expected to find text "Praise" in "Inquiry Inquiry successfully submitted Email: user@example.com Subject: You rock! Description: I really like your site!"
# ./spec/features/user_submits_inquiry_spec.rb:31
Note that whenever you add new attributes to a form, you must whitelist them.
To get this test to pass, we must allow the newly added category_id
to be present in our params for processing.
Modify the inquiry_params
method in our InquiriesController to permit the category_id
field:
def inquiry_params
params.require(:inquiry).permit([:email, :subject, :description, :category_id])
end
Running our test yields one final necessary tweak. Our show
page still has no idea about the added category information to Inquiry
. Let's fix that:
app/views/inquiries/show.html.erb
<h1>Inquiry</h1>
<p><%= notice %></p>
<p>Email: <%= @inquiry.email %></p>
<p>Subject: <%= @inquiry.subject %></p>
<p>Description: <%= @inquiry.description %></p>
<p>Category: <%= @inquiry.category.name %></p>
Run our feature specs and finally, green.
Running all of our specs, acceptance and model alike should yield green across the board at this point:
.............
Finished in 0.21495 seconds
13 examples, 0 failures
Commit and merge with master
git add -A . && git commit -m "Add inquiry submission with category"
git checkout master && git merge user-submits-inquiry-with-category