09 Jul 2015
Let's say we have a Todo model. The methods nameonly? and displaytext is purely presentational logic.
Class Todo < ActiveRecord::Base
has_many :taggings
has_many :tags, through :taggigngs
def name_only?
description.blank?
end
def display_text
name + tag_text
end
private
def tag_text
#some complicated regex code
end
We decide to move those two methods to a decorator:
#app/decorators/todo_decorator.rb
Class TodoDecorator
attr_reader :todo
def initialize(todo)
@todo = todo
end
def display_text
todo.name + tag_text
end
private
def tag_text
if todo.tags.any?
#some complicated regex code
end
end
Now we can remove the extracted code from the Todo model and update the way its called in the views:
#app/views/todos/new.html/haml
%ul
- @todos.each.do |f|
%li
= link_to TodoDecorator.new(todo).display_text, todo
If we find ourselves using the decorator alot, we could create a decorator method on the Todo model:
#app/models/todo.rb
def decorator
TodoDecorator.new(self)
end
We may in the future also want to call methods on the decorator and the original model and we don't want the views to be concerned with this level of logic (when to instantiate the decorator vs the model)
In the Todos controller, we could pass an array of decorators in addition to an array of Todos:
#app/controllers/todos_controller.rb
def index
@todos = current_user.todos.map(&:decorator)
end
But then our decorator has to respond to the methods within the decorator and within the model itself. To allow this we must extend Ruby the Forwardable module and create a delegator method:
#app/decorators/todo_decorator.rb
Class TodoDecorator
extend Forwardable
def delegators :todo, :name_only?
attr_reader :todo
def initialize(todo)
@todo = todo
end
def display_text
todo.name + tag_text
end
private
def tag_text
if todo.tags.any?
#some complicated regex code
end
end
Delegators are valuable when we present models one way in the database and differently in the browser - we use decorators to bridge this gap. When we seperate the domain logic from presentation logic, we can also test them seperately, creating decorator specs.
Draper
If you like decorators, you should check out Draper. Instead adding extendeding the Fowardable module, etc. All you have to do is create the decorator, make sure add 'delegae_all' and update the videos controller:
class VideoDecorator < Draper::Decorator
delegate_all
def rating
object.rating.present? ? "#{object.rating}/5.0" : "N/A"
end
end
class VideosController < ApplicationController
before_filter :require_user
def index
@videos = Video.all
@categories = Category.all
end
def show
@video = VideoDecorator.decorate(Video.find(params[:id]))
@reviews = @video.reviews
end
08 Jul 2015
Transactions and test database setup
If you look at the instructions for capybara here: https://github.com/jnicklas/capybara
In the "Transactions and database setup" section, it talks about the Selenium driver needs to run against a real HTTP server and that capybara will start one for you but it runs on a different thread. In this case using "transactions" as the strategy to reset the database after each spec can be a problem, and this is where you want to use the "database_cleaner" gem so that you can use the "truncation" strategy, which effectively just goes to your database and delete all the records. It's a bit slower than transaction rollbacks, but in this case guarantees data will be reset.
You can add the "databasecleaner" gem in your test group, and use the following configuration in your spechelper file inside of the RSpec.configure do |config| ... end block.
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :truncation
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
06 Jul 2015
To date, our feature tests have been fun using Capybara, which using RackTest. While it is very fast, by default - it does not support javascript. Adding ":js => true" to your test, will switch to the Capybara.javascript driver (Selenium Webdriver) - which will actually launch a browser to run the feature test.
#features/visitor_makes_payment.rb
require 'spec_helper'
feature 'Visitor makes payment', js: true do
background do
visit new_payment_path
end
scenario "valid card number" do
pay_with_credit_card('424242424242424242')
expect(page).to have_content('Thank you for your business!')'
end
scenairo "invalid card number" do
pay_with_credit_card('400000000000000069')
expect(page).to have_content('Your card has expired!')
end
scendario "declined card" do
pay_with_credit_card('4000000000000000002')
expect(page).to have_content('Your card was declined!')
end
end
def pay_with_crdit_card(card_number)
fill_in "Credit Card Number", with: card_number
fill_in "Security Code", with: "123"
select "3 - March", from: "date_month"
select "2015", from: 'date_year'
click_button "Submit Payment"
end
Tips:
Your Selenium Webdiver must be compatiable with your latest version of Firefox.
Since Selenium Webdriver is really slow, we will go with Capybara Webkit which is faster. This will require you to install the Capybrara Webkit gem and also install Qt locally (using homebrew). Make it the default js runner by declaring so in your spec_helper:
Capybara.javascript_driver = :webkit
To run tests with Selenium Webdriver so you can see what is happening in the web browser, simply add "driver: selenium" to the spec:
scenario "valid card number", driver: selenium do
Must install PhantomJs locally but it has a very nice feature set and allows for extensive customization.
05 Jul 2015
Stub Methods
Typically, you want to use the stub method in three scenarios:
1. You want to use the stub method when actually calling a method is expensive in terms of time. So you stub the test double to be in state that you desire.
The action or interface that you're testing is already thoroughly tested elsewhere. For example, there is no need to fully test the Stripe API's ablility to charge credi card and return token, becasue this funcitionality is already in the StripeWrapper::Charge test - therefore stubbing would be perfectly fine.
You want to mimick some behavior you haven't fully developed yet.
Doubles create a fake object that stands in the place of a real object and stubs call a desried fictitious method on that double and gives it a desired value as a result of calling that fictitous method.
We are intersted in doubles and stubs in this case because we want to avoid actually submtting a an charge and hitting Stripe's server.
#spec/controllers/payments_controller.rb
require 'spec_helper'
describe PaymentsController do
describe "POST create" do
context "with a successful charge" do
before do
charge = double('charge')
charge.stub(:successful?).and_return(true)
StripeWrapper::Charge.stub(:create).and_return(charge)
post :create, token: '123'
end
it "sets the flash success message" do
expect(flash[:success]).to eq("Thank you!")
end
it "redirects to the new payment path" do
expect(response).to redirect_to new_payment_path
end
end
context "with an failed charge" do
before do
charge = double('charge')
charge.stub(:successful?).and_return(false)
charge.stub(:error_message).and_return('Your card was declined')
StripeWrapper::Charge.stub(:create).and_return(charge)
post :create, token : '123'
end
it "sets the flash error message" do
expect(flash[:error]).to eq('Your card was declined')
end
it "redirects to the new payment path" do
expect(response).to redirect_to new_payment_path
end
end
end
#app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create
token = params[:stripeToken]
charge = StripeWrapper::Charge.create(amount: 3000, card: token)
if charge.successful?
flash[:succes] = "Thank you!"
redirect_to new_payment_path
else
flash[:error] = charge.error_message
redirect_to new_payment_path
end
end
end
Check out the Test Doubles docs for more info. For more info on Method Stubs, with an older version on Rspec, checkout these docs also.
04 Jul 2015
Isolated API Tests Stubbing Out the Internet
Hitting the Stripe server takes alot of time approximently 22 seconds sicnce we are hitting the server twice, once to grab the token. The answer to this is pre-script a response for a given request.
Webmock
Webmock allows us to do this by stubbing the HTTP client at the Library level and we can tell exactly what the response is. So we can stub the response using Webmock, always setting the response to a given value, but we would have to do this for every spec. It would be great to have something that gave us this fuctionality automatically and we also don't have to change our test code...
VCR
VCR integrates with Rspec and helps you to record your test suite's HTTP interactions and play them back automatically when you run future tests. Meaning, the first time the spec is run, the request will actually hit the TPA server, record the interaction and store it in a data file. The next time its run again... it will just play back the recorded file.
Getting Started
Step 1: Install both VCR & Webmock Gems
Step 2: Add Rspec Metadata (for VCR set up) to spec helper.rb
Metadata
require 'vcr'
VCR.configure do |c|
c.cassette_library_dir = 'spec/cassettes'
c.hook_into :webmock
c.configure_rspec_metadata!
end
RSpec.configure do |c|
# so we can use :vcr rather than :vcr => true;
# in RSpec 3 this will no longer be necessary.
config treat_symbols_as_metadata_keys_with_true_values = true (add to Rspec config block)
end
Step 3: Add :vcr to the transactions you want to record
it "charges the card successfully", :vcr do
response = StripeWapper::Charge.create(amount: 300, card: token)
expect(response).to be_successful
end
Step 4: Cassettes & Configs
As default, spec/cassettes holds all recordings and a creates data files for each spec ran. VCR allows for extensive customization. Including how many recordings per spec whether it be once or all.
Refactor
You can add Stripe.apikey = ENV['STRIPESECRET_KEY'] to your initializers (config/initializers/stripe.rb), rather than including in each test.