This won't be the most ground breaking of posts but I wanted to note down a lot of the common tasks I perform in the making of a rails application so I have a single point of reference.
When kicking off work on a rails project you'll either be starting completely fresh and could follow this sequence of commands to begin:
ruby -v
sqlite3 --version
gem install rails
rails new myproject
I like to work with PostgreSQL as my database because this is what I tend to work with in production and it's always a good idea to build your application using the same datbase in development as you would in production. This could be setup with the command: ```rails new myproject –database=postgresq
If you haven't worked with PostgreSQl before then this is a handy resourse to follow to get it setup on your machine.
Next up change the file at config/database.yml
to look something like this:
development:
adapter: postgresql
encoding: unicode
database: myproject_development
pool: 5
username: knoxjeffrey
password:
test:
adapter: postgresql
encoding: unicode
database: myproject_test
pool: 5
username: knoxjeffrey
password:
From the command line run rake db:create:all
to create the development and test databases.
The other option is to fork an existing project. Navigate to the project you wish to fork in GitHub and click "Fork" in the top right corner and choose where you wish to fork it. That's it, you have a remote copy but of course you'll want to create a local copy.
In your newly forked repository click the button on the right of the page to copy the address to clone your repo. In the terminal navigate to where you want the repo to be stored locally and enter the following:
mkdir ForkedProject
cd ForkedProject
git clone https://github.com/your-username/ForkedProject.git
You now have a local clone!
Want to sync your fork with the original repo? Go back to the original repo you forked from on GitHub and copy the clone url for the repo from the right hand side. Back in the terminal in your local copy type git remote -v
to see your current remote repositories which of course will be your own remote repository. To add addition remote repositories type:
git remote add upstream https://github.com/other-persons_repository/ForkedProject.git
Type git remote -v
again and you'll see the new repository added as an upstream branch. Good instructions for syncing a fork can be found here.
Before you install any Rails gems for your project make sure you have Bundler installed. Bundler provides a consistent environment for Ruby projects by tracking and installing the exact gems and versions that are needed.
gem install bundler
When you add gems to your Rails Gemfile it's as simple as bundle install
to install them for your project.
The gem byebug is a handy gem for debugging your Rails application and all you have to do is drop byebug into your code to inspect it at that point in the codebase.
Rails makes it super easy to create database tables. For example, if I want a users table in the database (all database table names are plural) then I can create a migration file with the terminal command rails g migration create_users
. This automatically creates a migration file with the following code:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
end
end
end
Within the create_table
block there are many options to add new columns to the database table and below I'll include several examples to show the different types and options:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false, unique: true
t.integer :age
t.text :about, default: ""
t.integer :credit, default: 0
t.references :organisation
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.timestamps
end
end
end
I'll quickly explain what some of the options mean. null: false
means that the entry cannot be NULL for that particular entry. unique: true
means that no two entries can have the same data for that column. default: ""
will automatically create an empty string for the data entry if noting has been entered. t.references :organisation
automatically creates a foreign key of organisation_id
and also ensures it is indexed. For more info about why you should index your foreign keys read this.
Below is a list of available types:
integer
primary_key
decimal
float
boolean
binary
string
text
date
time
datetime
timestamp
Don't forget to rake db:migrate
when you have finished editing your migrations.
To add a new column type the following example from the terminal rails g migration add_gender_to_users
class AddGenderToUsers < ActiveRecord::Migration
def change
add_column :users, :gender, :string
end
end
## Remove columns
To remove a column type the following example from the terminal rails g migration remove_gender_from_users
class RemoveGenderFromUsers < ActiveRecord::Migration
def change
remove_column :users, :gender
end
end
## Rename a table
To rename a table type ```rails g migration rename_organisation_table
class RenameOrganisationTable < ActiveRecord::Migration
def change
rename_table :organisations, :companies
end
end
## Rename a table column
To rename a table column ```rails g migration rename_users_email_column
class RenameUsersEmailColumn < ActiveRecord::Migration
def change
rename_column :users, :email, :email_address
end
end
# Rails models
Below is an example of how the implementation of a Rails model could look:
class User < ActiveRecord::Base
has_many :posts, -> { order start: :asc }
belongs_to :organisation
has_many :groups, -> { order group_name: :asc }, through: :user_groups
validates :email, presence: true, uniqueness: true
validates :name, presence: true
end
## Polymorphic associations
With polymorphic associations, a model can belong to more than one other model, on a single association. I've written a post on this already which can be found here.
For this example I have a posts
and categories
table. A post can have many categories and to record this I will use a post_categories
table. The basic setup of the models could look as follows:
class Post < ActiveRecord::Base
has_many :post_categories
has_many :categories, through: :post_categories
end
class Category < ActiveRecord::Base
has_many :post_categories
has_many :posts, through: :post_categories
end
class PostCategory < ActiveRecord::Base
belongs_to :post
belongs_to :category
end
## Changing relationship names
Sometimes you may wish to use a more descriptive term to describe the relationship between Rails models. For example is you have users
and posts
models the Post model would normally look as follows:
class Post < ActiveRecord::Base
belongs_to :user
has_many :post_categories
has_many :categories, through: :post_categories
end
However, it would be more descriptive to say that a post has a creator for example rather than a user. In this case the code would be:
class Post < ActiveRecord::Base
belongs_to :creator, foreign_key: 'user_id', class_name: 'User'
has_many :post_categories
has_many :categories, through: :post_categories
end
Nothing in the database has to change we are just renaming the relationship to use in Rails. Now rather than typing something like post.user
it would be post.creator
which is a lot more descriptive.
Seeding is extremely useful when developing your application. It allows you to populate your database with data to make it easy to test your application. The seeds file can be found under db/seeds.rb
and it's as simple as the following to add data:
User.create(email: 'knoxjeffrey@outlook.com', password: 'password', full_name: "Jeff Knox")
User.create(email: 'joe_bloggs@hotmail.com', password: 'password', full_name: "Joe Bloggs")
User.create(email: 'ann_other', password: 'password', full_name: "Ann Other")
Just keep following this pattern for any other data you would like in your testing database. There is a really useful gem called Fabrication which makes it really easy to create lots of fake data. For example if I want to create lots of random users I could simply type Fabricate.times(50, :users)
and this would create 50 new user objects for me in the database. I'll go into more details about Fabricator in the section about testing in this post.
The first time you create the seeds.rb
data you will need to type rake db:setup
from the terminal to populate the database. If you make any changes after that it is best to use rake db:reset
in order to clear out the database and then repopulate.
One of the things I've been trying to work on is keeping my Controllers skinny but also to standardise them as much as possible and use descriptive terms to allow the reader to quickly understand what the controller does. Here's a sample users controller:
class UsersController < ApplicationController
attr_reader :user
def index
set_users
end
def new
build_user
end
def create
build_user
if user.save!
flash[:notice] = "Welcome #{current_user.name}!"
redirect_to root_path
else
render :new
end
end
def show
set_user
end
def edit
set_user
end
def update
set_user
if user.update(user_params)
flash[:notice] = "Your details have been changed #{current_user.name}!"
redirect_to root_path
else
render :edit
end
end
private
def set_users
@users = User.all
end
def set_user
@user = User.find(params[:id])
end
def build_user
@user = User.new(user_params)
end
def user_params
if params.has_key?(:user)
params.require(:user).permit(:name, :email, :phone, :mobile)
else
{}
end
end
end
## Name spacing rails controllers
Name spacing is particularly useful is you several different roles on your site such as user, admin, moderator, etc. I have written a post on this previously which you can find here
A simple form_for in Rails could look as follows:
= form_for @post do |f|
= render 'shared/errors', controller_object: @post
.form-group
=f.label :title
= f.text_field :title, class: "form-control"
.form-group
= f.label :url
= f.text_field :url, class: "form-control"
.form-group
= f.label :description
= f.text_area :description, rows: 5, class: "form-control"
= f.submit @post.new_record? ? "Submit Post" : "Update Post", class: "btn btn-danger"
The file in shared/_errors.html.slim
would be as follows:
- if controller_object.errors.any?
.alert alert-danger
h5
= pluralize(controller_object.errors.count, 'error') %> in this form:
ul
- controller_object.errors.full_messages.each do |msg|
li= msg
A nice gem to help tidy up your forms if you're using Bootstrap is Rails bootstrap forms. The above would then look as follows:
= bootstrap_form_for @post do |f|
= f.text_field :title
= f.text_field :url
= f.text_area :description, rows: 5
= f.submit @post.new_record? ? "Submit Post" : "Update Post", class: "btn btn-danger"
## Name spaced forms
Sometimes you will have forms that are name spaced under admins for example and in that case the beginning of the form would be:
= bootstrap_form_for [:admins, @post] do |f|
## Additional form options
In the example below I have a form where a user can edit their details and also upload an avatar image. I have specified options such as the url and method:
= bootstrap_form_for @user, url: user_path(@user), method: :put, :html => { :multipart => true } do |f|
= f.email_field :email
= f.password_field :password
= f.text_field :full_name
= f.hidden_field :avatar_cache
= f.file_field :avatar, label: "Choose Your Profile Image"
= f.submit "Update", class: "btn bg-olive btn-flat btn-lg"
## Collection select
Below is an example of creating a list of check boxes in a form:
= f.collection_check_boxes :group_ids, current_user.groups, :id, :group_name, label: 'Request To Group'
When you use _ids
this means it is a polymorphic relationship and your strong params check in the controller would look something like the following:
params.require(:request).permit(:start, :finish, group_ids: [])
## Nested attributes
Nested attributes allows you to save attributes on associated records through the parent and has to be turned on by using the accepts_nested_attributes
class method eg:
class Booking < ActiveRecord::Base
has_many :event_bookings
has_many :events, through: :event_bookings
has_many :delegates, through: :event_bookings
...
accepts_nested_attributes_for :event_bookings
...
end
In the form we would need to use fields_for
which makes it possible to specify additional model objects in the same form. For example:
= bootstrap_form_for(@booking) do |f|
= f.hidden_field :booker_id, value: current_user.id
= f.fields_for :event_bookings do |ff|
= ff.collection_select :delegate_id, User.users_allowed_to_attend_conferences, :id, :name
= ff.hidden_field :event_id, value: @event.id
The controller strong params code would then look something like this:
params.require(:booking).permit(:booker_id, event_bookings_attributes: [:event_id, :delegate_id])
This would result in a new entry being created in the bookings
and event_bookings
tables.
As mentioned earlier, I use the Fabricator gem for generating new objects. With Fabricator I can create my object generators in the spec/fabricators
folder. A User object for example would be stored at spec/fabricators/user_fabricator.rb
and could look as follows:
Fabricator(:user) do
email { Faker::Internet.email }
password { Faker::Internet.password(6) }
full_name { Faker::Name.name }
end
Note that I'm using the Faker gem which is really handy to greate randomly generated data for things like names, emails, dates and a whole lot more.
One other thing I've done is to generalise my object generator by creating a new file under spec/support/factory_helper.rb
which looks as follows:
def object_generator(*args)
Fabricate(*args)
end
def generate_attributes_for(*args)
Fabricate.attributes_for(*args)
end
This means that if I ever change the gem I'm using to generate objects I can easily change the commands in one place rather than have to go through my entire test suite and make changes there.
Here is an example snippet of one of my specs for a user model which is found at spec/models/user_spec.rb
:
require 'spec_helper'
describe User do
it { should have_many(:reviews).order(created_at: :desc) }
it { should have_many(:queue_items).order(list_position: :asc) }
it { should validate_presence_of :email_address }
it { should validate_presence_of :password }
it { should validate_presence_of :full_name }
it { should validate_uniqueness_of :email_address }
it { should_not allow_value("test@test").for(:email_address) }
it { should validate_length_of(:password).is_at_least(5) }
describe :queue_item_exists? do
let(:user) { object_generator(:user) }
let(:video) { object_generator(:video) }
it "returns true if current user already has video in the queue" do
object_generator(:queue_item, user: user, video: video)
expect(user.queue_item_exists?(video)).to be true
end
it "returns false if current does not have video in the queue" do
object_generator(:queue_item, video: video)
expect(user.queue_item_exists?(video)).to be false
end
end
...
end
Here are some other simple examples when writing the expect code to test the outcome:
expect(user.waiting_and_accepted_requests).to eq([request2])
expect(user.waiting_and_accepted_requests).to match_array([request1, request2])
expect(user.waiting_and_accepted_requests).to eq([])
expect(Request.first.status).to eq('expired')
expect(tokens_array.sum).to eq(46)
## Controller specs
Whilst I initially started out with controller testing I have recently started to move away from them, preferring to test at a higher level with Capybara for example.
I use Capybara for my feature specs which allows me to test my web application by simulating how a real user would interact with my app. One thing to note is that you cannot test JavaScript with Capybara but I'll get onto that in the next section.
The first thing I do for my feature specs is to create some helper methods for frequently used actions such as signing in and out for example. I have to make a small addition in my spec_helper.rb
file:
...
RSpec.configure do |config|
config.include FeatureSessionHelper, type: :feature
...
and then in spec/support/feature_session_helper.rb
I have the following example code:
# only for features. Creates a user and signs them in so they are on the home path
module FeatureSessionHelper
def sign_in_user(a_user=nil)
a_user ||= object_generator(:user)
visit sign_in_path
fill_in :email_address, with: a_user.email_address
fill_in :password, with: a_user.password
click_button "Sign In"
expect(current_path).to eq(home_path)
end
def sign_out
visit sign_out_path
end
end
I can then create a spec to test the user sign in process at spec/features/user_sign_in_spec.rb
:
require 'spec_helper'
feature "user signs in" do
given(:valid_user) { object_generator(:user) }
scenario "with existing email and correct password" do
sign_in_user(valid_user)
expect_drop_down_to_contain_full_name(valid_user)
end
scenario "with incorrect login details" do
visit sign_in_path
enter_incorrect_login_details(valid_user)
click_button "Sign In"
expect_to_return_to_sign_in_page_and_see_error
end
def expect_drop_down_to_contain_full_name(user)
expect(page.find('.dropdown .dropdown-toggle').text).to have_content user.full_name
end
def enter_incorrect_login_details(user)
fill_in "Email", with: user.email
fill_in "Password", with: "totally wrong password"
end
def expect_to_return_to_sign_in_page_and_see_error
expect(current_path).to eq(sign_in_path)
expect(page).to have_content "There is a problem with your username or password"
end
end
Note that, like my controllers, I try to keep my scenarios as descriptive as possible so they are easy to understand at a glance.
Capybara has the handy command save_and_open_page
to allow you to visually see what's happening in the browser which can be really useful for debugging.
To allow CSS and Javascript to be loaded when we use save_and_open_page, the development server must be running at localhost:3000 as specified below or wherever you want. See original issue here and final resolution here. Add the following to spec_helper.rb
Capybara.asset_host = "http://localhost:3000"
## Tests with JavaScript
For tests with JS I prefer to use the poltergeist gem. In order to switch to the Capybara js driver in your tests you can do the following at the beginning in your scenario:
scenario "user successfully invites friend and is accepted", js: true do
Selenium is the default but as mentioned I prefer Poltergeist so I need to set this up in spec_helper.rb
:
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: true)
end
Sometimes you'll setup records in the test DB, only to have the feature tests act like those records never existed. A way to solve this is with the database cleaner gem and the setup in spec_helper.rb
looks as follows:
RSpec.configure do |config|
...
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
config.use_transactional_fixtures = false
...
end