Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jboursiquot/8251315 to your computer and use it in GitHub Desktop.
Save jboursiquot/8251315 to your computer and use it in GitHub Desktop.

Inquiries Walkthrough

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.

User Stories

Submit an inquiry

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

Categorize inquiries

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)

Setup

Follow the steps below to get the application set up to the point where we can start creating acceptance tests.

Generate new rails app

rails new inquiries -T -d postgresql

Git Init

git init && git add . && git commit -m "First commit"

Create .ruby-gemset and .ruby-version

rvm --ruby-version use 2.0.0@inquiries --create
git add . && git commit -m "Add ruby version and gemset"

Resources:

Update Gemfile

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"

Configure Database Settings

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"

Create and Migrate Database

rake db:create && rake db:migrate

Commit:

git add . && git commit -m "Setup database and add schema"

Install RSpec

rails g rspec:install

Verify install:

rake spec

Commit:

git add . && git commit -m "Install RSpec"

You're now ready for first User Story.


Submit Inquiry 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 and spec/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 the new 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

Categorize inquiries

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment