Build Robust & Production Quality Applications - Lesson 8: Beyond MVC - Decorators

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

Build Robust & Production Quality Applications - Lesson 8: Transactions & Test Database Setup

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

Build Robust & Production Quality Applications - Lesson 8: Feature Tests with Javascript

Selenium Webdriver

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.

Capybara Webkit

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

Poltergeist

Must install PhantomJs locally but it has a very nice feature set and allows for extensive customization.

Build Robust & Production Quality Applications - Lesson 8: Fully Integrated API Tests

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.

  1. 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.

  2. 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.

Build Robust & Production Quality Applications - Lesson 8: Fully Integrated API Tests

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.