Creating a Rails 4 app from scratch following TDD, using Rspec
I am going ahead to create a simple Product Catalog type rails project. I will follow test driven development methodology to build up this application. By default Rails use test unit as testing framework, but like most of rails developer I prefer Rspec.
I am going to use mongoDB with this project (I love mongo, that’s why !). To use mongoDB, most of the configuration comes down by making sure that I am not loading ActiveRecord. One way to avoid loading ActiveRecord at time of creating rails project using following switch
—skip-active-record
The Rails command that generate application skeleton now has an option -O , which commands rails to skip active record.
So, the final app skeleton will look like so:
rails new product-catalog -T -O
Now open the Gemfile and specify required gem for this project. I am going to use rspec-rails for this project to write test. To automate the testing I am going to use Guard. Guard handle events on file system modifications. I am also going to use Capybara gem, which simulates the user, open up a page and test automatically. mongoid and bson_ext as I am going to use mongoDB. To get the latest mongoid gem, i am declaring GitHub as an another Rubygems sources (they have stopped building gems now), and bundler will look for mongoid gems in the github the sources.
So, the final modified part of Gemfile will look like so:
source 'http://gems.github.com
gem 'mongoid', github: 'mongoid/mongoid'
gem 'bson_ext'
group :development, :test do
gem 'rspec-rails'
gem 'guard-rspec'
gem 'capybara'
end
Now I need to run bundle install or only
bundle
Before I start building the application, I need rspec ready. To do so, install rspec
rails g rspec:install
Create an integration test for test driven development:
rails g integration-test product-catalog
Next, i am going ahead to initialize guard, which will automate my testing.
guard init rspec
Lets test it now
guard
Issues !
When I started using Rspec with mongoid in rails 4, I started having problems. The first was the following error message:
undefined method `fixture_path=‘ for # (NoMethodError)
just needed to comment out the line in spec_helper.rb:
config.fixture_path = "#{::Rails.root}/spec/fixtures"
Another error:
__ undefined method `use_transactional_fixtures=’ for # (NoMethodError)__
just needed to comment out the line spec_helper.rb:
config.use_transactional_fixtures = true
Now run
guard
It will automatically run test and it is failing the tests ….. WOW (we have’t write any thing yet, those tests are by default generated when we generate integration tests for our app)
Let’s build the app #
Listing Product #
I am going to write test as a sort of guideline for what to be build. In spec/requests/product_catelog_spec.rb write:
describe "GET /products" do
it "display some products list" do
visit products_path
page.should have_content "Nexus 5"
end
end
Issues !
This time Capybara creates an issue:
undefined method 'visit'
As per the Capybara Documentation
Capybara is no longer supported in
request specs as of Capybara 2.0.0. The recommended way to use Capybara is with feature specs.
To solve this issue we could move tests to spec/features folder or include Capybara::DSL for request specs.
#spec_helper.rb
RSpec.configure do |config|
...
config.include Capybara::DSL
...
end
now guard will run and I got
Failures:
1) ProductsCatalog GET /products display some products list
Failure/Error: visit products_path
NameError:
undefined local variable or method `products_path'
Don’t worry its fine, this path will only be created if we create necessary routes. If I do rake routes
it will be empty, we have’t created any restful routes yet. We know for a Product in a Product Catalog we are going to show ‘em, add 'em edit 'em, delete 'them and update 'em. we can do this Restful Routes by using resources in config/routes.rb.
resources :products
By adding this single line in to routes.rb file if will generate 9 restful routes for our application.
Prefix | Verb | URI Pattern | ControllerAction |
---|---|---|---|
products | GET | /products(.:format) | products#index |
POST | /products(.:format) | products#create | |
new_product | GET | /products/new(.:format) | products#new |
edit_product | GET | /products/:id/edit(.:format) | products#edit |
product | GET | /products/:id(.:format) | products#show |
PATCH | /products/:id(.:format) | products#update | |
PUT | /products/:id(.:format) | products#update | |
DELETE | /products/:id(.:format) | products#destroy |
Let’s go back and see what guard is saying:
Failures:
1) ExploreRacks GET /products display some products list
Failure/Error: visit products_path
ActionController::RoutingError:
uninitialized constant ProductsController
Because I have’t created any controller yet. As MVC guide line controller name will be plural. I am going to name my controller as Products and create index action. lets do that:
rails g controller products index
Let’s check guard:
Failures:
1) ExploreRacks GET /products display some products list
Failure/Error: page.should have_content "Nexus 5"
expected #has_content?("Nexus 5") to return true, got false
Guard is expecting to see “Nexus 5” product on the product list page. To achieve the same I need to set up a data base. I will do that by saying:
rails g model product name:string price:integer
As I am using mongoid, generator will invoke mongoid and generate a model that looks like:
class Product
include Mongoid::Document
field :name, type: String
field :price, type: Integer
end
Now we need to generate the Mongoid configuration file. We’ll create the mongoid file by running the following command:
rails g mongoid:config
It will configure available database sessions, and defines the name of the default database that Mongoid can connect to.
To automate our testing, let’s create a test Product in product_spec file, final modified spec looks like so:
Guard is expecting to see product in index.html.erb
but it did’t find anything there. So let’s open Products controller and fetch all product from the data base and store 'em into an instance variable, to access it from view.
class ProductsController < ApplicationController
def index
@products = Product.all
end
end
Now we have products object and we are going to display each of them in view:
<ul>
<% @products.each do |product| %>
<li>
<%= product.name %>
<%= product.price %>
</li>
<% end %>
</ul>
And finally guard stop complaining
1 example, 0 failures
Create Product #
Next we are going to add a new product into our product catalog
Just like wire frame sketch I designed the activity like so:
it "add a product into product catalog" do
visit products_path
click_button 'New Product'
current_path.should == new_product_path
fill_in :Name, :with => 'Nexus 5'
fill_in :Price, :with => '30000'
click_button 'Save Product'
current_path.should == products_path
end
To make happy guard first I add a link 'New Product’ into index page.
<%= link_to 'New Product', new_product_path%>
The link_to method is Rail’s built-in view helper, it creates a hyperlink and with a path to new product path
Now guard saying that:
AbstractController::ActionNotFound:
The action 'new' could not be found for ProductsController
We have’t created suitable action in controller, lets do that, define a new method like this:
def new
end
Now guard saying that
ActionView::MissingTemplate:
Missing template products/new, application/new with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}.
It indicates that template is missing. In this case Rails will first look for this template, if not found, then it will attempt to load a template called application/new. It looks for this template here because the ProductsController inherits from ApplicationController.
Now I am going to create a new file at app/views/posts/new.html.erb and writing the form for creating new product in it. To create a form for Products in this page we will going use rails form builder, provided by a helper method called form_for
<%= form_for :product, url: products_path do |f|%>
<%= f.label :Name %>
<%= f.text_field :name %>
<%= f.label :Price %>
<%= f.text_field :price %>
<%= f.submit %>
<% end %>
the products_path helper with the :url option point the form to the create action of the current controller, the ProductsController, and will send a POST request to that route
Now guard reporting that:
Failure/Error: click_button 'Save Product'
AbstractController::ActionNotFound:
The action 'create' could not be found for ProductsController
Lets deal with that:
The params method returns an ActiveSupport::HashWithIndifferentAccess object, which allows you to access the keys of the hash using either strings or symbols. this class is intended for use cases where strings or symbols are the expected keys and it is convenient to understand both as the same.
It provides an interface for protecting attributes from end-user assignment. This makes Action Controller parameters forbidden to be used in Active Model mass assignment until they have been whitelisted.
ActionController::StrongParameters module has an public method params, which returns a new ActionController::Parameters object that has been instantiated with the request.parameters and the class ActionController::Parameters is a subclass of class ActiveSupport::HashWithIndifferentAccess which is a subclass of Hash ….:P
Allows to choose which attributes should be white-listed for mass updating and thus prevent accidentally exposing that which shouldn’t be exposed. Provides two methods for this purpose: require and permit. The former is used to mark parameters as required. The latter is used to set the parameter as permitted and limit which attributes should be allowed for mass updating.
Ensures that a parameter is present. If it’s present, returns the parameter at the given key, otherwise raises an ActionController::ParameterMissing error.
rb> ActionController::Parameters.new(product: {name: 'Moto G'}).require(:product) => {"name"=>"Moto G" irb> ActionController::Parameters.new(product: {}).require(:product) ActionController::ParameterMissing: param is missing or the value is empty: product
Returns a new ActionController::Parameters instance that includes only the given filters and sets the permitted attribute for the object to true. This is useful for limiting which attributes should be allowed for mass updating
irb> params = ActionController::Parameters.new(products: {name: 'Moto G', price: '14000'}) => {"products"=>{"name"=>"Moto G", "price"=>"14000"}} irb> permitted = params.require(:products).permit(:name) => {"name"=>"Moto G"} irb> permitted.has_key?(:name) => true irb> permitted.has_key?(:price) => false
We are going far from the app, let’s get back to work.
When submit button is pressed, along with other parameter this
"product"=>{"name"=>"Sim Adopter", "price"=>"12"}
form data is submitted to controller. We are going to save it into data base.
To do so we need to write in product controller:
def create
@product = Product.new(product_params)
@product.save
redirect_to @product
end
private
def product_params
params.require(:product).permit(:name, :price)
end
Showing Product #
we can add a show action to display details of a product.
first write the test for it:
it "show details of a product" do
@product = Product.create :name => 'Nexus 4', :price => 25000
visit product_path(@product)
page.should have_content 'Nexus 4'
page.should have_content '25000'
end
Now satisfy guard:
def show
@product = Product.find(params[:id])
end
add a corresponding view show.html.erb
<h1>Product Details</h1>
<p>
<strong>Name:</strong>
<%= @product.name %>
</p>
<p>
<strong>Price:</strong>
<%= @product.price %>
</p>
Updating Product #
As we need @product object multiple time, lets move it to a before block and clean all after test .
before do
@product = Product.create :name => 'Nexus 5', :price => 30000
end
after do
Product.delete_all
end
Draw a out line of edit page:
it "edit a Product details" do
visit products_path
find("#product_#{@product.id}").click_link 'Edit'
current_path.should == edit_product_path(@product)
find_field('Name').value.should eq 'Nexus 5'
find_field('Price').value.should eq '30000'
fill_in 'Price', :with => '25000'
click_button 'Save Product'
page.should have_content '25000'
visit products_path
page.should have_no_content '30000'
end
Now guard is saying that it can’t see product with id product_product.id. Lets change the index.html and add an id field with each product
so the final index.html.erb will look like so:
<% @products.each do |product| %>
<tr id="product_<%= product.id %>">
<td><%= product.name %></td>
<td><%= product.price %></td>
</tr>
<% end %>
Now guard saying that it can’t find the link ‘Edit’, lets add a link to each of the product in index.html.erb
<td><%= link_to 'Edit', edit_product_path(product) %></td>
Guard not able to found edit action in ProductsController. lets do it
def edit
@product = Product.find(params[:id])
end
My edit.html.erb template look like so:
<%= form_for :product, url: product_path(@product), method: :patch do |f|%>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :price %>
<%= f.text_field :price %>
<%= f.submit %>
<% end %>
we are pointing the form to the update action.
The method: :patch option tells Rails that we want this form to be submitted via the PATCH HTTP method which is the HTTP method you’re expected to use to update resources according to the REST protocol.
Now guard saying that The action ‘update’ could not be found for ProductsController, lets add that:
def update
product = Product.find(params[:id])
if product.update(product_params)
redirect_to products_path
else
render 'edit'
end
end
Deleting Product #
now we are going to delete product from catalog
it "delete a product" do
visit products_path
find("#product_#{@product.id}").click_link 'Delete'
page.should have_no_content 'Nexus 5'
page.should have_no_content 'Nexus 5'
current_path.should == products_path
end
guard is now asking for a link in index page lets add that and define destory method in controller.
in index.html.erb
<td><%= link_to 'Delete', product_path(product), method: :delete, data: {confirm: 'Are you sure?'} %></td>
in product controller
def destroy
product = Product.find(params[:id])
product.destroy
redirect_to products_path
end