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 postgresqlgit init && git add . && git commit -m "First commit"rvm --ruby-version use 2.0.0@inquiries --creategit 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'
endbundle installgit add . && git commit -m "Add testing gems"Add config/database.yml to .gitignore
echo "config/database.yml" >> .gitignorecp config/database.yml config/database.example.ymlModify 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:migrateCommit:
git add . && git commit -m "Setup database and add schema"rails g rspec:installVerify install:
rake specCommit:
git add . && git commit -m "Install RSpec"You're now ready for first User Story.
Create a feature branch
git checkout -b user-submits-inquiryCreate 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'
endRun RSpec
rspec spec/features/user_submits_inquiry_spec.rbYou 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!'
endRun 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:16You 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 inquiriesRemove the extraneous
spec/controllersandspec/helpersdirectories.
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
endThe
newaction 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
endRun 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
Inquirymodel. Let's generate one.
Generate an Inquiry model
rails g model inquiry email subject description:textAttempting 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)}
endNow 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
endRun our migration
rake db:migrate && rake db:rollback && rake db:migrate && rake db:test:prepareRun your inquiry model spec:
rspec spec/models/inquiry_spec.rbFix 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
endNote 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 failuresWe'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 InquiriesControllerWe'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
endNote the protected
inquiry_paramsmethod 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
showpage and provide a flash notice to be shown on that page. Should the create fail, we send the client back to thenewpage.
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.rbThis 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])
endFinally, 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")
endAs 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-inquiryWe'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-categoryChange our feature spec
In spec/features/user_submits_inquiry_spec.rb:
- add the new acceptance criteria to our list
- use
selectto 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'
endRun 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
endRun 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:integerPerform our migration:
rake db:migrate && rake db:rollback && rake db:migrate && rake db:test:prepareRun 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 nameSet 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) }
endapp/models/category.rb
class Category < ActiveRecord::Base
has_many :inquiries, inverse_of: :category
validates_presence_of :name
endAll tests should be green after preparing the
Categorymodel.
...........
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'
endRun 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])
endRunning 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