07 Mar 2015
Items Covered:
- Feature Specs
- Capybara
- Request Specs
Types of Testing We've Covered:
- Models (Model Specs)
- Controllers (Controller Specs)
- Views, Routes, Helpers, Mailers (Feature Specs)
Feature Specs
Feature Specs cover all the above in integration, although there are also specs
for every item listed above, however mailers, routes, views, and helpers do not need to be tested in isolation.
With feature specs, you mimic the user's experience originating with the browser.
This is the first example of vertical integration.
Specs are typically broken into features.
There is something called "request specs" which focuses more horizontal integration. This is when you want to test multiple requests and responses across various controllers, making sure that things flow in a sequence.
In this course, we will NOT use requestspecs, becasue typically requestspecs are used to capture
business processes from end to end. However any business process worth measuring must be
centered around the customer expereince...
Capybara
The Capybara gem is the way you can simulate user interaction with the browser.
Load RSpec 2.x support by adding the following line (typically to your spec_helper.rb file):
If you are using Rails, put your Capybara specs in spec/features.
If you are not using Rails, tag all the example groups in which you want to use Capybara with :type => :feature.
You can now write your specs like so:
describe "the signin process", :type => :feature do
before :each do
User.make(:email => 'user@example.com', :password => 'password')
end
it "signs me in" do
visit '/sessions/new'
within("#session") do
fill_in 'Email', :with => 'user@example.com'
fill_in 'Password', :with => 'password'
end
click_button 'Sign in'
expect(page).to have_content 'Success'
end
end
Use :js => true to switch to the Capybara.javascript_driver (:selenium by default), or provide a :driver option to switch to one specific driver. For example:
describe 'some stuff which requires js', :js => true do
it 'will use the default js driver'
it 'will switch to one specific driver', :driver => :webkit
end
**Capybara Built in DSL (reads more like UAT - User Accpetance tests) - reads more higher level
- feature is in fact just an alias for describe ...,
- :type => :feature,
- background is an alias for before, scenario for it,
- and given/given! aliases for let/let!, respectively.
Finally, Capybara also comes with a built in DSL for creating descriptive acceptance tests:
feature "Signing in" do
background do
User.make(:email => 'user@example.com', :password => 'caplin')
end
scenario "Signing in with correct credentials" do
visit '/sessions/new'
within("#session") do
fill_in 'Email', :with => 'user@example.com'
fill_in 'Password', :with => 'caplin'
end
click_button 'Sign in'
expect(page).to have_content 'Success'
end
given(:other_user) { User.make(:email => 'other@example.com', :password => 'rous') }
scenario "Signing in as another user" do
visit '/sessions/new'
within("#session") do
fill_in 'Email', :with => other_user.email
fill_in 'Password', :with => other_user.password
end
click_button 'Sign in'
expect(page).to have_content 'Invalid email or password'
end
end
Capybara Drivers
RackTest is really fast, and does not really fire-up the browser. It's a headless driver
therefore doesn't really simulate the browser experience. RackTest does not support Js.
Selenuim is a popular driver that does handle Js and fire-ups Firefox.
But this option is very slow.
Capybara Webkit is another headless driver that can execute Js well.
It's much faster than selenium but slower than RackTest.
First feature spec
require 'spec/helper'
feature 'User signs in' do
background do
User.create(username: "john", full_name: "John Doe")
end
scenario "with existing username" do
visit root_path
fill_in "Username", with: "john"
click_button "Sign in"
page.should have_content "John Doe"
end
end
The 'fill_ in "Username", ' is referring to the labeltag :username, "Username"
- you can also refer to fill in the label tag, the name, or the input_ id,
although its best practice to use the label tag because it easier to read.
Request Specs
Watch RailsCast video on Capybara & Request Specs
- Request Specs are alternatives to the Rails Builtin Integration Testing
(Refer to RailsCast video 187)
In terminal, run:
1. rails g rspec:install
2. rails g intergration_test task
3. Open up task_spec, code should be fine as is, but you would
change the request verb and the assertion
4. rake spec:requests
Example:
describe "Tasks" do
describe "GET /tasks" do
it "displays tasks" do
Task.create!(:name => "paint fence")
get tasks_path
response.body.should include("paint fence")
end
end
end
describe "POST /tasks" do
it "create task" do
post_via_redirect tasks_path, tasks: {name: "mow lawn"}
response.body.should include("mow lawn")
end
end
end
One issue with request specs this that they dont mimic the user's experience
becasue you'e submitting the request directly, rather than going through the form
like the user would - that is why we use Capybara (rather than webrat) & Launchy
Installing Capybara, allows us to change our existing tests:
describe "Tasks" do
describe "GET /tasks" do
it "displays tasks" do
Task.create!(:name => "paint fence")
visit tasks_path
page.should include("paint fence")
end
end
end
describe "POST /tasks" do
it "create task" do
visit tasks_path
fill_in "New Task", with: "mow lawn"
click_button "Add"
page.should have_content("successfully added task")
page.should have_content("mow lawn")
end
end
end
*What if the test fails and you don't know why?
Thats where the gem "Launchy" comes in:
-anywhere in the test, you can write:
Capybara does not test Javascript with out Selenium:
it "supports js", js: true do
visit tasks_path
click_link "test js"
page.should have_content("js works")
end
end
Let's try adding Js to our first test and see if it is supported by
Selenium.
describe "Tasks" do
describe "GET /tasks" do, js: true do
it "displays tasks" do
Task.create!(:name => "paint fence")
visit tasks_path
page.should include("paint fence")
end
end
end
The test will fail because Selenium does not support database transactions
In #spec_helper.rb:
Change config.use_transactional_ fixtures = true, to false.
Selenium does not support database transactions.
But this will mean our database transactions will carryover in between
tests and we dont want that. So the answer is the database cleaner
gem that will run in between tests. You must add the config code
from the documentation into the spec_helper, removing transactions since the
arent supported by Selenuim.
To run pry in a spec, you must use:
require 'pry'; binding.pry before the error/after the action
During pry. you can use
Launchy "save_and_open_page"
05 Mar 2015
Items Covered:
- Structural Refactor
- Skinny Controller, Fat Models
- Rspec Macros
- Rspec Shared Examples
Structural Refactor
Controllers are more of traffic control and methods containing logic should refarctored to the model level
- For simple refactors such as moving methods from controllers to methods,
since your test suite is already comprehensive enough to cover the current model, you dont have to create additional tests at the model level, only if you decide to add additional functionality.
Typically you shouldn't have more than one conditional or return from a method within a method. You should refactor by moving a piece of the code to private method.
Skinny Controller, Fat Models
One of the most common refactors in Rails and one of the most well-known architectural principals in rails.
Fat Models, means the models assume the most responsibilty.
Good blog post to read: Skinny Models, Fat Controllers
Here - James talks about moving responsibilty from the view, to the controller, and then to the model.
Examples of Opensource Project Management Tools that have need to go on a Fat Controller Diet:
ChilliProject
Redmine
Refactoring to fat models in MyFlix
Tip: When refactoring your controllers to fat models, do not move methods that take parameters in a form
because that is the purpose of the controller - to talk to the views and models.
Look for those that iterate through the model.
Rspec Macros
Allows you to extract logic that can be used by multiple rspec controllers.
So rather than the following at the begining of each controller test:
before do
frank = Fabricate(:user)
session[:user_id] = frank.id
end
We could create a macro, spec/support/macros.rb and include the above code:
def set_current_user
frank = Fabricate(:user)
session[:user_id] = frank.id
end
def current_user
User.find(session[user_id])
end
AND in your test:
before {set_current_user}
Now, if I want to clear the current user to test the path of an unauthorized user
create another macro called clearcurrentuser, make it the first line in your test.
def clear_current_user
session[user_id] = nil
end
Shared Examples
What bout you want to test the redirect for unauthenticated users for not just index but index and new
Create a Shared Example:
#spec/support/shares_examples.rb
shared_examples "require_sign_in" do
it "redirects to the front page" do
response.should redirect_to root_path
end
end
In your test: you can't just write:
it behaves_like "require_sign_in"
because you have to clear the current user and hit the action first:
context "user not signed in" do
before do
clear_current_user
get :index
end
it behaves_like "require_sign_in"
end
But this is frustrating becasue although we DRY'd up our code a little,
we still have to add a before block for the clearcurrentuser and the index.
It would be great if there's a way to pass the action to the shared example, and there is:
shared_examples "require_sign_in" do
it "redirects to the front page" do
clear_current_user
action
response.should redirect_to root_path
end
end
AND in your test:
context "not_signed_in" do
it_behaves_like "require_sign_in" do
let(:action) { get :index }
end
end
AND you actually don't need the context anymore:
<index>
it_behaves_like "require_sign_in" do
let(:action) { get :index }
end
<new>
it_behaves_like "require_sign_in" do
let(:action) { get :new }
end
<create>
it_behaves_like "require_sign_in" do
let(:action) { post :create, todo: {name: 'something'} }
end
Shared examples can be used on both controller and model levels
For more info:
Shared Examples Docs
02 Mar 2015
Items Covered:
- Growing Complexity
- Interactive Debugging for Solution Delivery
- Transactions
Growing Complexity Guided By Tests
Tags.all.map(&:name) == Tags.all.map{|tag| tag.name}
def create
@todo = Todo.new(params[:todo])
if @todo.save
location_string = @todo.name.split('at').last.strip
@todo.tags.create(name: "location#(location_string)")
redrect_to root_path
else
render :new
end
end
strip method removes all spaces.
def create
@todo = Todo.new(params[:todo])
if @todo.save
location_string = @todo.name.split('at').last.strip
locations = location_string.split('and')
locations.each do |location|
@todo.tags.create(name: "location:#(location)")
redrect_to root_path
else
render :new
end
end
When to use each vs map
each is used for when you want to iterate through a an collection of data, an
array for example but you dont want to manipulate the result.
map is ideal for when iterating over a collection and then you need to alter of manipulate those returned values for input into another problem.
When to use split vs regex
Use split when searching for one element
And regex when searching for more than one.
Interactive Debugging for Solution Discovery
After writing and implementing your test cases, you should run sanity checks within your browser.
If bugs are identified, you should run write tests to isolate it the bug.
Use Binding.pry to in your controller being tested, after the problematic line.
Binding.pry is really just irb so you now have a means to experiment with your code
until you find the right solution.
For a good regex tutorial:
Regex Tutorial
Rubular
.split method cannot run on nil so Ruby 1.9 introduced .try(:split),
which will run .split if not nil.
Transactions
Wrapping batch operations into transactions:
To update the queue in MyFlix, the position entered must be an integer.
If not, the entire page will fail to update. This is unlike normal transactions
where we can include a validation error.
This is where the concept of transactions come in. If all actions in the transaction
do not save, it will rollback.
Transactions are protective blocks where SQL statements are only permanent if
they can all succeed as one atomic action. The classic example is a transfer between
two accounts where you can only have a deposit if the withdrawal succeeded and vice versa.
Transactions enforce the integrity of the database and guard the data against program errors or database break-downs.
So basically you should use transaction blocks whenever you have a number of statements that must be executed together
or not at all.
28 Feb 2015
Use Faker to Generate Fake Data
Creating fake data when your model has uniqueness validations will throw errors
if your data is not unique.
Fabricator(:todo) do
name { "cooking" }
user
end
Fabricator(:user) do
email { "joe@example.com"} #This will throw an error eventually if email record already exists.
end
Examples
Creating a category:
Fabricator(:category) do
name { Faker::Name.name }
end
Creating a video:
Fabricator(:video) do
title { Faker::Lorem.words(5).join(" ") }
description { Faker::Lorem.paragraph(2) }
end
Creating an invitation:
Fabricator(:invitation) do
recipient_name { Faker::Name.name }
recipient_email { Faker::Internet.email }
message { Faker::Lorem.paragraphs(2).join(" ") }
end
Read more: Faker Docs
26 Feb 2015
It's typical to create many objects during testing and the more attributes you have
the more cumbersome it could be to maintain, moreover, if an attribute changes, it then becomes very difficult
to update all those tests.
To make this better, we can use an object generator framework called Fabrication.
Steps:
1. Create a folder under specs called "Fabricators"
2. create fabricator_todo.rb
Fabricator(:todo) do
name { "cooking" }
end
3. You can also create a user each time a todo is created:
Fabricator(:todo) do
name { "cooking" }
user { Fabricate(:user) }
end
or
Fabricator(:todo) do
name { "cooking" }
user
end
In your test suite:
If you want to create an object in your database:
describe "#display_text" do
let(:todo) { fabricate(:todo) }
subject { todo.display_text }
If you would rather create an object in memory, you could:
describe "#display_text" do
let(:todo) { fabricate.build(:todo) }
subject { todo.display_text }