Rails Ing Guide

User Manual:

Open the PDF directly: View PDF PDF.
Page Count: 14

DownloadRails Ing Guide
Open PDF In BrowserView PDF
Rails Testing Guide
TimeCop Usages
Model Testing
Matchers
Validations
Relationships
Testing standard rails functions
Schema Testing
On delete
Cascade
Nullify
Restrict
Controller Testing
Input permutations
Valid vs invalid params
Authenticated access
Authorized access
Matchers
View Testing
Job Testing
Functional / Integration Testing
General
Capybara Toolbox
Here’s a quick overview of methods that do wait:
methods that don’t wait:
Navigating
Clicking links and buttons
Interacting with forms
Querying
Finding
Scoping
Working with windows
Capybara provides some methods to ease finding and switching windows:
Scripting
Modals
Debugging
Extra Resources

General
TimeCop Usages
around(​​:example​​) ​do​​ |example|
Timecop.freeze(today) ​do
example.run
​end
end
Timecop.freeze ​do
expect(subject.remote_created_at.to_i).to eq(Time.now.to_i)
expect(subject.remote_updated_at.to_i).to eq(Time.now.to_i)
end

Scheduling
RSpec.describe Model ​do
describe ​"#perform_later"​ ​do
it ​"uploads a backup"​ ​do
expect {
model.schedule_job
}.to
have_enqueued_job.with(​'backup'​).on_queue(​"low"​).at(Date.tomorrow.noon)
​end
​end
end

Model Testing
●
●
●
●
●

Ensure to test all custom methods
Ensure to test any custom validators
Ensure to test any complex unique constraints
Ensure to test with_priviledge and use ​this​ as a good example
Ensure to test all scopes fully

describe User, ​'.active'​ ​do
it ​'returns only active users'​ ​do
​# setup
active_user = create(​:user​, ​active:​ ​true​)
non_active_user = create(​:user​, ​active:​ ​false​)

​# exercise
result = User.active
​# verify
expect(result).to eq [active_user]
​# teardown is handled for you by RSpec
​ nd
e
end

Matchers
expect(thing).to be_valid
expect(thing).to be_invalid
expect(user.errors.full_messages.first).to eql(​'Email has already
been taken'​)

Validations
●
●
●

Ensure to test the boundaries and not the rails in-built functions directly e.g. no need
for shoulda matchers
Ensure to test the foreign key dependencies
Ensure to test on delete
○ t.​belongs_to :​ model foreign_key​​: { ​on_delete​​: ​:cascade ​}
○ t.​belongs_to :​ model​​, ​foreign_key​​: { ​on_delete​​: ​:nullify ​}
○ t.​belongs_to :​ model​​, ​foreign_key​​: { ​on_delete​​: ​:restrict ​}

Relationships
●
●

Ensure to filter out deleted objects from the has_many by default
When dealing with potentially many records on a relation

scope ​:active​​, -> { where(​​status:​​ ACTIVE) }
has_many ​:user_organizations​​, -> { active }, ​autosave:​​ ​true​​, ​inverse_of:​​ ​:user

Testing standard rails functions
FactoryGirl.create ​:user​, ​email:​ ​'admin@example.com'
user = FactoryGirl.build ​:user​, ​email:​ ​'admin@example.com'
expect(user.errors.full_messages.first).to eql(​'Email has already been taken'​)

Schema Testing
●

Do not use dependant: :destroy if you have a ​foreign_key: {​on_delete​​: ​:cascade​​} on the
schema

On delete
Cascade

# t.belongs_to :time_off_policy, foreign_key: { on_delete: :cascade }
describe ​'validations'​​ ​do
context ​'schema'​​ ​do
before { time_off_policy }
context ​'time_off_policy relationship'​​ ​do
it ​'should remove time off when policy deleted'​​ ​do
expect { time_off_policy.destroy }.to change(TimeOffPolicyUser, ​:count​​).by(-​​1​)
​end
​end
​end
end

Nullify

# t.integer :created_by_id, foreign_key: { references: :users, on_delete: :nullify }
describe ​'validations'​​ ​do
context ​'schema'​​ ​do
before { time_off_policy }
context ​'user relationship'​​ ​do
it ​'should nullify created_by_id when associated user is deleted'​​ ​do
expect { user.destroy }.to change { time_off_policy.reload.created_by_id
}.from(user.id).to(​​nil​​)
​end
​end
​end
end

Restrict
# t.belongs_to :time_off_policy, index: true, foreign_key: { on_update: :restrict,
on_delete: :restrict }
describe ​'validations'​​ ​do
context ​'schema'​​ ​do
context ​'time_off_requests relationship'​​ ​do
it ​'should not remove time off when policy deleted'​​ ​do
FactoryGirl.create ​:time_off_request​​, ​time_off_policy:​​ time_off_policy
expect { time_off_policy.destroy }.to raise_error ActiveRecord::InvalidForeignKey
​end
​end
​end
end

Controller Testing
●

Ensure to include security checks (fully understand how to use ​declarative
authorization​) summarized ​here

describe ​'security checks'​ d
​ o
context ​'as an org owner'​ d
​ o
let(​:org_role​) {​:owner​}
​end
context ​'as an org manager'​ ​do
let(​:org_role​) {​:manager​}
​end
context ​'as an org user'​ ​do
let(​:org_role​) {​:user​}
context ​'and not a project member'​ ​do
​end
context ​'and a project user'​ ​do
let(​:proj_role​) {​:user​}
​end
context ​'and a project manager'​ ​do
let(​:proj_role​) {​:manager​}
​end
context ​'and a project viewer'​ ​do
let(​:proj_role​) {​:viewer​}
​end
​end
end

Input permutations
describe ​'GET #index'​​ ​do
context ​'when params[:filter_by] == first_name'​​ ​do
it ​'filters results by first_name'
​end

context ​'when params[:filter_by] == last_name'​​ ​do
it ​'filters results by last_name'
​end
end

Valid vs invalid params
describe ​'POST #create'​​ ​do
context ​'with valid params'​​ ​do
it ​'redirects to show page'
​end
context ​'with invalid params'​​ ​do
it ​'re-renders #new form'
​end
end

Authenticated access
describe ​'GET #index '​​ ​do
context ​'when user is logged in'​​ ​do
it ​'renders the listing page'
​end
context ​'when user is logged out'​​ ​do
it ​'redirects to login page'
​end
end

Authorized access
describe ​'GET #show'​​ ​do
context ​'as content owner'​​ ​do
it ​'renders the permalink template'
​end
context ​'as an admin'​​ ​do
it ​'renders the permalink template'
​end
context ​'as a guest user'​​ ​do
it ​'displays access forbidden message'
​end
end

Matchers
expect(response).to redirect_to(location)
expect(response).to have_http_status(​:created​)
expect(assigns(​:widget​)).to be_a_new(Widget)
expect(assigns(​:product​)).to eq(bestseller)
expect(assigns(​:widgets​)).to eq([widget1, widget2, widget3])
expect(flash[​:notice​]).to eq ​"Congratulations on buying our stuff!"
# change in active record
expect { post_with user, :
​ activity​ }.to change(Activity, ​:count​).by(​1​)
# change static method
Date.beginning_of_week = ​:sunday
expect { controller.send(​:reset_beginning_of_week​) }.to change(Date,
:beginning_of_week​).to(​:monday​)
# change in instance method
expect {
delete_with org_owner, ​:destroy​, params
client.reload
}.to change(client, ​:status​).to(Client::STATUS_ARCHIVED)

View Testing
●

You want to test all logical paths
○ Authorization
○ Contents of elements

# subject should always render the view and return rendered which returns the resultant
view as text
subject { render ​partial:​​ ​'dashboard/new_dashboard_alerts.html.slim'​​; rendered }
before ​do
assign(​​:delete_screens_allowed​​, [​​11​​]) ​# artificially assign any required params
subject
end
# rendered view includes slicer
expect(subject).to match /slicer/
expect(subject).to have_css(​​'.state_shot.no_screenshot'​​)
# you can use this for example when you are not a manager / owner logic
expect(rendered).to_not have_css(​​'.state_shot.browser_time'​​)
# RSpec matcher for whether the element(s) matching a given selector exist

expect(rendered).to have_selector(​​"li a"​​, ​text:​​ ​'Overview'​​)
# RSpec matcher for text on the page
expect(subject).to have_text(​​',"Sun, Dec 3, 2017 12:00 pm"]'​​)
# RSpec matcher for links
expect(rendered).to have_link(​​'Cancel'​​, ​href:​​ invoices_url)
assert_select ​'td'​​, ​text:​​ ​"#{project.name} Archived"

Mailer Testing
RSpec.describe SomeMailer ​do
let(​:mail​)
{ ProjectMailer.send_something(arg1, arg2) }
let(​:text_part​) { mail.text_part.body.raw_source }
let(​:html_part​) { mail.html_part.body.raw_source }
it ​'renders the headers'​ ​do
expect(subject.subject).to eq(​'A new payment from Org A was
completed'​)
expect(subject.from).to eq([​'support@hubstaff.com'​])
expect(subject.reply_to).to eq([​'support@hubstaff.com'​])
expect(subject.bcc).to eq([owner.email])
​end
it ​'should have a link to view budget'​ ​do
expect(html_part).to match(​/View budget/​)
expect(html_part).to match(​/#{project_url(project)}/​)
​end
it ​'should have a link to project page'​ ​do
expect(text_part).to match(​/#{project_url(project)}/​)
​end
end

Job Testing
●

Ensure to test the scheduling of a job

#### ActiveJob
RSpec.describe Model ​do
describe ​"#perform_later"​ ​do
it ​"uploads a backup"​ ​do

expect {
model.schedule_job
}.to
have_enqueued_job.with(​'backup'​).on_queue(​"low"​).at(Date.tomorrow.noon)
​end
​end
end
#### Sidekiq
it ​'creates a background job'​ ​do
expect{ subject }.to change(Sidekiq::Extensions::DelayedMailer.jobs,
:size​).by(​1​)
end

Functional / Integration Testing
General
Integration is known also as black box testing which essentially means you test without the
need for prior knowledge of how it works. We are using Capybara and Rspec to fulfill the role
of testing the UI interactions and bridges the gap between ruby & JS testing.
I am going to step you through how to create integration specs.
1. We generally create one spec per page e.g. /login interactions go into its own spec
a. e.g. login_page_spec.rb
2. When creating capybara specs it is important to have a good understanding of where
and when to use asynchronous calls vs synchronous calls in order to ensure the
specs run in a timely fashion.
3. Ensure to extract common functionality into methods or if you are using them across
multiple specs think of adding them spec/support/features.rb
4. Follow ​this​ as an excellent example for creating Capybara specs

Capybara Toolbox
Here’s a quick overview of methods that do wait:
●
●
●
●

find, find_field, find_link and other finders
within
has_selector?, has_no_selector? and similar
click_link, fill_in, check, select, choose and other actions

methods that don’t wait:
●
●
●
●
●
●

visit(path)
current_path
all(selector)
first(selector)
execute_script, evaluate_script
simple accessors: text, value, title, etc

Directly interacting with JavaScript

find(​".active"​) ​# Ensures the element is present before continuing
execute_script(​"$('.active').focus()"​) ​# only use execute_script
as last resort when capybara does not support what you are trying
to achieve
Checking a fields value

expect(page).to have_field(​"Username"​, ​with:​ ​"Joe"​) ​# wait until
matching field has value or fail if exceeds wait time
Checking an element’s attribute

expect(page).to have_css(​".user[data-name='Joe']"​) ​# capybara will
wait for the element to appear and verifies whether or not it has
the expected value
Looking for matching CSS

it "
​ doesn't have an active class name"​ ​do
expect(page).not_to have_active_class ​# passes immediately if
not on the page or waits up to the time wait for it to disappear
end
def​ ​have_active_class
have_css(​".active"​)
end

Navigating
visit root_path

Clicking links and buttons
click_link(​'id-of-link'​)
click_link(​'Link Text'​)
click_button(​'Save'​)
click_on(​'Link Text'​) ​# clicks on either links or buttons
click_on(​'Button Value'​)

Interacting with forms
fill_in(​'First Name'​, ​with:​ ​'John'​)
fill_in(​'Password'​, ​with:​ ​'Seekrit'​)
fill_in(​'Description'​, ​with:​ ​'Really Long Text...'​)
choose(​'A Radio Button'​)
check(​'A Checkbox'​)
uncheck(​'A Checkbox'​)
attach_file(​'Image'​, ​'/path/to/image.jpg'​)
select(​'Option'​, ​from:​ ​'Select Box'​)

Querying
page.has_selector?(​'table tr'​)
page.has_selector?(​:xpath​, '
​ .//table/tr'​)
page.has_xpath?(​'.//table/tr'​)
page.has_css?(​'table tr.foo'​)
page.has_content?(​'foo'​)
# Examples
expect(page).to have_selector(​'table tr'​)
expect(page).to have_selector(​:xpath​, ​'.//table/tr'​)
expect(page).to have_xpath(​'.//table/tr'​)
expect(page).to have_css(​'table tr.foo'​)

expect(page).to have_content(​'foo'​)

Finding
find_field(​'First Name'​).value
find_field(​id:​ ​'my_field'​).value
find_link(​'Hello'​, ​:visible​ => ​:all​).visible?
find_link(​class​: ['​some_class​', '​some_other_class​'], :​visible​ =>
:​all​).​visible?
find_button(​'Send'​).click
find_button(​value:​ ​'1234'​).click
find(​:xpath​, ​".//table/tr"​).click
find(​"#overlay"​).find(​"h1"​).click
all(​'a'​).each { |a| a[​:href​] }
# Examples
find_field(​'First Name'​){ |el| el[​'data-xyz'​] == ​'123'​ }
find(​"#img_loading"​){ |img| img[​'complete'​] == ​true​ }
find(​'#navigation'​).click_link(​'Home'​)
expect(find(​'#navigation'​)).to have_button(​'Sign out'​)

Scoping
within(​"li#employee"​) ​do
fill_in ​'Name'​, ​with:​ ​'Jimmy'
end
within(​:xpath​, ​".//li[@id='employee']"​) ​do
fill_in ​'Name'​, ​with:​ ​'Jimmy'
end
# Examples
within_fieldset(​'Employee'​) ​do
fill_in ​'Name'​, ​with:​ ​'Jimmy'
end
within_table(​'Employee'​) ​do
fill_in ​'Name'​, ​with:​ ​'Jimmy'
end

Working with windows
Capybara provides some methods to ease finding and switching
windows:
facebook_window = window_opened_by ​do
click_button ​'Like'
end
within_window facebook_window ​do
find(​'#login_email'​).set(​'a@example.com'​)
find(​'#login_password'​).set(​'qwerty'​)
click_button ​'Submit'
end

Scripting
page.execute_script(​"$('body').empty()"​)
result = page.evaluate_script(​'4 + 4'​);

Modals
accept_alert ​do
click_link(​'Show Alert'​)
end
dismiss_confirm ​do
click_link(​'Show Confirm'​)
end
accept_prompt(​with:​ ​'Linus Torvalds'​) ​do
click_link(​'Show Prompt About Linux'​)
end
message = accept_prompt(​with:​ '
​ Linus Torvalds'​) ​do
click_link(​'Show Prompt About Linux'​)
end
expect(message).to eq(​'Who is the chief architect of Linux?'​)

Debugging
save_and_open_page ​# take screenshot when page opens
print page.html ​# print snapshot of DOM
page.save_screenshot(​'screenshot.png'​)
Save_and_open_screenshot ​# save screenshot & open file

Extra Resources
●
●
●
●
●
●

https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capy
bara
http://www.rubydoc.info/github/jnicklas/capybara
https://www.varvet.com/blog/simple-tricks-to-clean-up-your-capybara-tests/
https://www.simplybusiness.co.uk/about-us/tech/2015/02/flaky-tests-and-capybara-be
st-practices/
https://www.sitepoint.com/basics-capybara-improving-tests/
https://semaphoreci.com/community/tutorials/5-tips-for-more-effective-capybara-tests



Source Exif Data:
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.5
Linearized                      : Yes
Producer                        : Skia/PDF m72
Page Count                      : 14
EXIF Metadata provided by EXIF.tools

Navigation menu