RSpec

activeresource model rspec

Given: certain API to obtain merchant information: “/service/merchant/:merchantId”
When: it is called with the merchant id,
Then: it returns a json response like the following:

{"store": {
  "preferences" : [{
    "settings" : [
      {
        "key": "logo",
        "value": "logo.png"
      },
      {
        "key": "text",
        "value": "Hi, welcome to our store"
      },
      {
        "key": "url",
        "value": "http://somestore.com"
      }]
    }]
}}

Then: turn the store settings into a hash like the following:

{:logo => "logo.png", :text =>"Hi, welcome to our store", :url => "http://somestore.com"}

Assuming that we already have a method called get_merchant_info that performs the above task – turning the json response into a hash containing the information.

rspec

describe "#get_merchant_info" do
  context "when the service returns a 200 - data found" do
    it "returns a Hash containing the merchant's information" do
      api_data = {:logo => "logo.png", :text =>"Hi, welcome to our store", :url => "http://somestore.com"}
      
      # settings = mock('settings', <TODO: hash that resembles the json response>  )
      store = mock('store', {:preferences =>[settings]})
      MerchantService.any_instance.should_receive(:store).at_least(1).times.and_return(store)

      merchant = MerchantService.new
      merchant.get_merchant_info.should be_kind_of(Hash)
      merchant.get_merchant_info.should eq(api_data)
    end
  end
end

note: MerchantService is the model that integrates with the API and receives the json response.

The reason I set up api_data at the beginning is so I can reuse it for both json response and the hash result.

Now how do we turn api_data into a hash that resembles the json response (for line 6 above)?

After taking a look at Enumerable – http://apidock.com/ruby/Enumerable, I decided to use collect

So I test it out via the console/irb:

api_data.collect{|k,v| {"key" => k.to_s, "value" => v}}
 => [{"key"=>"logo", "value"=>"logo.png"}, {"key"=>"text", "value"=>"Hi, welcome to our store"}, {"key"=>"url", "value"=>"http://somesite.com"}] 

yayyy! Just what we’re looking for.

But is our test efficient? Personally, I don’t like how we’re mocking twice and stubbing the response, why don’t we set up the response like so…

describe "#get_merchant_info" do
  context "when the service returns a 200 - data found" do
    it "returns a Hash containing the merchant's information" do
      api_data = {:logo => "logo.png", :text =>"Hi, welcome to our store", :url => "http://somestore.com"}
      merchant = MerchantService.new({:store =>
                                          {:preferences => [{
                                                :settings =>
                                                    api_data.collect { |k, v| {"key" => k.to_s, "value" => v} }
                                           }]} })
      merchant.get_merchant_info.should be_kind_of(Hash)
      merchant.get_merchant_info.should eq(api_data)
    end
  end
end

which way do you think is better? hmm? or maybe you can think of a way that’s even better? let me know (;

testing cookies is almost as fun as eating them

Given: we have two different apps A and B
And: we GET data X from backend (Java) API
When: we pass “secret” data X from app A to app B
Then: we use a secure cookie to store data X

#controller

def index
    begin
      data = Data.find(@id)
      data_X = data['X']
      if data['X'] == "Keep"
        cookies[:X] = {
          :value => "true",
          :secure => true,
          :domain => '.www.cheriecodes.com',
          :httponly => true
        }
      elsif data['X'] == "Remove"
        cookies[:X] = {
          :value => "false",
          :secure => true,
          :domain => '.www.cheriecodes.com',
          :httponly => true
        }
      end

    # some other code here that directs to app B #
  
    rescue ActiveResource::ResourceNotFound => e
      Rails.logger.debug "#index: fails to find data"
    rescue => e
      Rails.logger.debug "#index error: #{e.message}"
    end
  end

note: yes, we should take out the cookie into its own method to keep it DRY, but that’s not my topic today d: so ahem… how do we test for cookies?

#rspec

context "when data X is equal to 'Keep'" do
    it "sets the cookie X to true" do
        Data.stub(:find).and_return({'X' => "Keep"})
        get :index
        cookies[:X].should == "true"
    end
end

but it fails miserably…

Failure/Error: cookies[:X].should == "true"
       expected: "true"
            got: nil (using ==)

What? How’s that possible?? Hmm… let’s get a better view of the whole spec

describe "#index" do
    before :each do
        @mocked_cookies = mock('cookie store').as_null_object
        controller.stub(:cookies).and_return(@mocked_cookies)
        @mocked_cookies.stub(:[]).with(:country_preference).and_return(nil)
    end

    # other specs #

    context "when data X is equal to 'Keep'" do
        it "sets the cookie X to true" do
          Data.stub(:find).and_return({'X' => "Keep"})
          get :index
          cookies[:X].should == "true"
        end
    end
end

Ahhhhhhhhhhh… somebody already stubbed the cookies to return nil

so to fix this, separate out the two different specs into its own describe block and move the mocked cookies into the appropriate describe block (not the one that tests for cookies[:X]) and the test passes (:

Side Question: can you think of any other way to pass “secret” (private) information between app A and app B?

default argument value & nil guard

suppose we have this following method, which we call up to 3 times for 3 different types of policy.

  def return_policy_link(type, country)
    policy_urls ||= Cherie::TermsAndConditions.get_policy_urls(country.upcase)
    custom_url(policy_urls[type])
  end

But what if country is “dynamic”?
As in: return_policy_link(“terms”, cookies[:country_preference])
cookies[:country_preference] is updated according to the user’s selection, browser, etc…

anyway, point is, we don’t know what country could be…

Let’s update our country argument to default to “US”, just in case country is missing:

  def return_policy_link(type, country="US")
    policy_urls ||= Cherie::TermsAndConditions.get_policy_urls(country.upcase)
    custom_url(policy_urls[type])
  end

kay, is that good enough?

Let’s run our rspec!

    describe "#return_policy_link" do
      before :each do
        us_links = {"electronicDisclosure" => "pages/electronic_communication", "terms" => "pages/terms", "privacy" => "pages/privacy"}
        Cherie::TermsAndConditions.stub(:get_policy_urls).with("US").and_return(us_links)
      end

      context "get terms policy link for US" do
        it "returns the right url" do
          controller.return_policy_link("terms", "US").should include("pages/terms")
        end
      end
      context "get terms policy link for missing country" do
        it "returns the terms link for US" do
          controller.return_policy_link("terms").should include("pages/terms")
        end
      end
    end

so far so good, but what if country is nil?

   context "get terms policy link when country is nil" do
     it "returns the terms link for US" do
       controller.return_policy_link("terms", nil).should include("pages/terms")
     end
   end

rspec fails ):

we need a nil guard and still default to “US”

  def return_policy_link(type, country = "US")
    country ||= "US"
    policy_urls ||= Cherie::TermsAndConditions.get_policy_urls(country.upcase)
    custom_url(policy_urls[type])
  end

alrighty, now all the tests pass (:

Hmm… can we refactor rspec?

    describe "#return_policy_link" do
      let(:terms_link) {{"terms" => "pages/terms"}}

      before :each do
        Cherie::TermsAndConditions.stub(:get_policy_urls).and_return(terms_link)
      end

      context "returns the expected terms link with type as terms and country" do
        it "is US" do
          controller.return_policy_link("terms", "US").should include(terms_link["terms"])
        end
        it "is missing" do
          controller.return_policy_link("terms").should include(terms_link["terms"])
        end
        it "is nil" do
          controller.return_policy_link("terms", nil).should include(terms_link["terms"])
        end
      end

not sure if I am really digging the new description, even tho it shows up nicely as:
#return_policy_link returns the expected terms link when type is terms and country is US
what do you think?

Language Selector

TASK:

  • Make a language selector for the user to choose which language he/she would like to see the page in.
  • note: The language selector only shows a list of languages that we support.

EX:

  • 1. In the language selector, show the default language (A) selected.
  • 2. When the user clicks on the current language (A), display a list of other supported languages (B,C,D,etc).
  • 3.  When the user selects a new language (B), hide the list of supported languages.
  • 4. When the user clicks on the current language (B), display a list of other supported languages (A,C,D,etc).

———————————————————————————————————————

let’s look at the view first for an overview:
# _language_selector.html.haml

#current_language{:onClick => "language_selector();"}
  .language_list.hidden
      %ul
        -other_languages(assumed_language).each do |x|
          %li
            =link_to t(x), :bank => params[:bank], :locale => language_to_locale(x), :country => Project::I18n.country_for_locale(language_to_locale(x))
   =t(assumed_language)
   =image_tag('blue_arrow_off.png', :class=> "language_arrow", :id=> "off_arrow")
   =image_tag('gold_arrow_on.png', :class=>"hidden language_arrow", :id=> "on_arrow")

# language_selector javascript:

function language_selector(){
    $('.language_list, #off_arrow, #on_arrow').toggle();
    $('#current_language').toggleClass("selected_language");
}

# sass related to the language_selector

.language_arrow{
  margin: 0 5px;
}

.language_selector{
  color: #007EAC;
  cursor: pointer;
  position: absolute;
  text-align: right;
}

#current_language {
  @extend .language_selector;
  li{
    position: relative;
  }
}

.language_list {
  @extend .language_selector;
  right: 5px;
  border: 1px solid #666;
  padding: 2px 16px 2px 35px;
  bottom: 15px;
  li{
    text-align: right;
  }
}

.selected_language{
  color: #FF0000;
}

.language_list (haml div) contains the list of languages that we support except the language selected already.

assumed_language is the language selected already, we say “assumed” here because we’re not sure if that is the preferred language yet when the user lands on the page for the first time.

selected_language is a color style (orange/gold) that is applied to the current language when the list of other languages is displayed, so that it matches with the on_arrow (orange/gold).

so… how did we get that language_list and assumed_language?

# language_helper.rb

include ActionView::Helpers::TranslationHelper

module LanguageHelper
  SUPPORTED_LANGUAGE = {
    # language => default locale
      "English" => :en,
      "Français" => :"fr-CA"
  }

  DEFAULT_LANGUAGE = "English"
  DEFAULT_LOCALE = I18n.default_locale

  #example: given locale :en or :en_CA, returns "en"
  def get_language(locale)
    locale.to_s[0..1] if locale
  end

  #returns the language given the locale, example :fr #=> "Français"
  def convert_locale_to_language(locale)
    given_locale = get_language(locale)
    # x returns an array of ["language", :locale]
    SUPPORTED_LANGUAGE.each do |x|

      #returns "en" from :en_CA
      shortened_locale = get_language(x[1])

      if shortened_locale == given_locale
        return x[0]
      end
    end

    #returns default language if all else fails
    DEFAULT_LANGUAGE
  end

  #example: given cookie[:locale] set up from app controller, returns "en"
  def assumed_language
    locale = cookies[:locale] ? get_language(cookies[:locale]) : DEFAULT_LOCALE
    convert_locale_to_language(locale)
  end

  def supported_languages
    SUPPORTED_LANGUAGE.keys
  end

  def other_languages(current_language = DEFAULT_LANGUAGE)
    supported_languages - current_language.to_a
  end

  def language_to_locale(language)
    SUPPORTED_LANGUAGE[language]
  end

end

remember to rspec

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
include ApplicationHelper

describe LanguageHelper do

  before :each do
    @locale = [:en, :en_CA, :"en-CA", "en", "en_CA", "en-CA"]
    @french_locale = [:fr, :"fr-CA", "fr"]
    @unsupported_locale = [:sp, :gr, "sp_MX", "sw_AF"]
  end

  describe "get_language" do
    it "should return the english locale as a string with only two characters" do
      @locale.each do |x|
        get_language(x).should eq("en")
      end
    end

    it "should return the french locale as a string with only two characters" do
      @french_locale.each do |x|
        get_language(x).should eq("fr")
      end
    end
  end

  describe "convert_locale_to_language" do
    it "should return the perspective language according to the locale if supported" do
      @locale.each do |x|
        convert_locale_to_language(x).should eq("English")
      end
    end

    it "should return French language given the French locale" do
      @french_locale.each do |x|
        convert_locale_to_language(x).should eq("Français")
      end
    end

    it "should return the default language if locale is not yet supported" do
      @unsupported_locale.each do |x|
        convert_locale_to_language(x).should eq("English")
      end
    end
  end

  describe "assumed_language" do
    context "obtain the assumed language from the locale cookie set up by application controller" do
      it "should return the language in English from the cookie with :en as its value" do
        helper.request.cookies[:locale] = :en
        assumed_language.should eq("English")
      end

      it "should return the language in French from the cookie with :fr as its value" do
        helper.request.cookies[:locale] = :fr
        assumed_language.should eq("Français")
      end
    end

    it "should set English as the default language if the cookie is missing" do
      assumed_language.should eq("English")
    end
  end

  describe "supported_languages" do
    it "should be an array" do
      supported_languages.should be_kind_of(Array)
    end
  end

  describe "other_language" do
    context "returns an array of supported languages except the language that is already selected" do
      it "should be an array" do
        other_languages.should be_kind_of(Array)
      end

      it "should not include the selected languages" do
        other_languages("English").should_not include("English")
        other_languages("Français").should_not include("Français")
      end
    end
  end

end

please enlighten me if you see code that I can improve on, thanks (:

GET a list of countries & more

GET

  • /countries

EX RESPONSE:

  • {“countryList”:[
  •       {“countryName”:”United States”,”countryCode”:”US”,”isoCode”:”840″},
  •       {“countryName”:”Canada”,”countryCode”:”CA”,”isoCode”:”124″}]
  • }

TASK:

  • retrieve the list of countries and format it, display countryName in specified language, etc.

——————————————————————————————————————-

module Project

  class ListCountries

    class << self       
      def current_list         
        begin           
          list_countries_url = APP_CONFIG['list_countries_url']           
          list_countries_headers = {               
              'Content-Type' => 'application/json',
              'Accept' => 'application/json',
              APP_CONFIG['api_key_header'] => APP_CONFIG['api_key']
          }
          response, body = APIClient.common_services_get(list_countries_url, list_countries_headers)
          ## APIClient makes HTTP calls with SSL set up (if uri.scheme == 'https') and headers and key and all that good stuff... ##
          if body
            JSON.parse(body)["countryList"]
          end
        rescue => e
          Rails.logger.debug "Project::ListCountries.current_list rescue => #{e.message}"
          raise
        end
      end
      #returns the list of countries
      # => [{"countryName"=>"United States", "countryCode"=>"US", "isoCode"=>"840"},
      #     {"countryName"=>"Canada", "countryCode"=>"CA", "isoCode"=>"124"}]

      def country_names
        current_list.collect{|x| x["countryName"]} unless current_list.nil?
      end
      #returns an array of country names
      # => ["United States", "Canada"]

      def alpha_country_codes
        current_list.collect{|x| x["countryCode"]} unless current_list.nil?
      end
      #returns an array of alpha country names
      # => ["US", "CA"]

      def iso_country_codes
        current_list.collect{|x| x["isoCode"]} unless current_list.nil?
      end
      #returns an array of iso country code
      # => ["840", "124"]

      def translated_country
        begin
          I18n.t(alpha_country_codes, :scope => "countries", :raise => true)
        rescue => e
          Rails.logger.debug "Project::ListCountries.translated_country rescue => #{e.message}"
          raise
        end
      end
      # returns translated countries
      # => ["United States", "Canada"] from en.yml
      # => ["États-Unis", "Canada"] from fr-CA.yml

      def translated_names_to_alpha
        unless current_list.nil?
          names_to_alpha = translated_country.each_with_index.collect{|country, index| {country => alpha_country_codes[index]}}
          names_to_alpha.inject{|k,v| k.merge v}
        end
      end
      # returns a Hash of translated country names to their alpha country codes
      # => {"United States" => "US", "Canada" => "CA"}
    end
  end
end

sample RSPEC:

require 'spec_helper'

describe "ListCountries" do

  context "successful response" do

    before(:each) do
      list_countries = [OpenStruct.new(:code => "200"), {"countryList" =>
                         [{"countryName" => "United States", "countryCode" => "US", "isoCode" => "840"},
                          {"countryName" => "Canada", "countryCode" => "CA", "isoCode" => "124"}]}.to_json]
      APIClient.stub(:common_services_get).and_return(list_countries)
    end


    describe "current_list" do
      it "should return an array of a list of countries" do
        result = Project::ListCountries.current_list
        result.should be_kind_of Array
      end
    end

    describe "country_names" do
      it "should return an array of country names" do
        result = Project::ListCountries.country_names
        result.should be_kind_of Array
        result.should include("United States")
      end
    end

    describe "translated_country" do
      it "should return an array of translated country names" do
        Project::ListCountries.stub(:alpha_country_codes).and_return(%w{US CA})
        Project::ListCountries.translated_country.should == %w{United\ States Canada}
      end

      it "should raise an exception when the country is not translated" do
        Project::ListCountries.stub(:alpha_country_codes).and_return(%w{SG LA})
        lambda { Project::ListCountries.translated_country }.should raise_error
      end
    end

    describe "translated_names_to_alpha_codes" do
      it "should return a hash of translated country names to their alpha country codes" do
        Project::ListCountries.stub(:alpha_country_codes).and_return(%w{US CA})
        Project::ListCountries.stub(:translated_country).and_return(%w{United\ States Canada})
        result = Project::ListCountries.translated_names_to_alpha
        result.should == {"United States" => "US", "Canada" => "CA"}
        result.should be_kind_of Hash
      end
    end
  end
end

Route Constraints

TASK:

  • constraint T&C policy routes according to its parameters.

EX:

  • /en/us/legal/terms.f0a6f2aa-8e68-45ae-a875-84444ef1130f

constraints:

  • languageCode – 2 alpha characters – EX: en/ fr/ sp
  • country – 2 alpha characters – EX: us/ ca/ uk
  • policy – 3 types – EX: Terms/ Privacy/ Electronic
  • guid – standard – 32 hexadecimal digits – EX: 21EC2020-3AEA-1069-A2DD-08002B30309D

—————————————————————————————————————–
rspec:
note: the guid is located after the “.”, which works as a separator for formatted routes,
hence the :format=>guid instead of :guid => guid.

describe "GET policies" do
    it "should route to us privacy page" do
      { :get => '/en/us/legal/privacy.f0a6f2aa-8e68-45ae-a875-84444ef1130f'}.should
      route_to(:controller=>"pages", :action=>"privacy")
    end

    it "should route to ca terms page" do
      { :get => '/en/ca/legal/terms.f0a6f2aa-8e68-45ae-a875-84444ef1130f'}.should
      route_to(:controller=>"pages", :action=>"terms_ca")
    end

    it "should route to 404 page when given invalid policy parameter" do
      { :get => '/en/ca/legal/invalid.f0a6f2aa-8e68-45ae-a875-84444ef1130f'}.should
      route_to(:controller=> "errors", :action => "routing",
               :path=>"en/ca/legal/invalid",
               :format=> "f0a6f2aa-8e68-45ae-a875-84444ef1130f")
    end

    it "should route to 404 page when given invalid country parameter" do
      { :get => '/en/invalid/legal/terms.f0a6f2aa-8e68-45ae-a875-84444ef1130f'}.should
      route_to(:controller=> "errors", :action => "routing",
               :path=>"en/invalid/legal/terms",
               :format=> "f0a6f2aa-8e68-45ae-a875-84444ef1130f")
    end

    it "should route to 404 page when given invalid language parameter" do
      { :get => '/invalid/us/legal/privacy.f0a6f2aa-8e68-45ae-a875-84444ef1130f'}.should
      route_to(:controller=> "errors", :action => "routing",
               :path=>"invalid/ca/legal/privacy",
               :format=> "f0a6f2aa-8e68-45ae-a875-84444ef1130f")
    end

    it "should route to 404 page when given invalid guid" do
      { :get => '/en/us/legal/privacy.invalid'}.should
      route_to(:controller=> "errors", :action => "routing",
               :path=>"en/ca/legal/privacy",
               :format=> "invalid")
    end

  end

# routes.rb

constraints(PolicyConstraint) do
   get ':language/:country/legal/:policy.:guid' => "pages#policy_by_type",
        :constraints => {:language => /[a-zA-Z]{2}/, :country => /[a-zA-Z]{2}/,
        :guid=>/[\h]{8}-[\h]{4}-[\h]{4}-[\h]{4}-[\h]{12}/}

end

An example of policy_by_type method that is used for filtering two countries: CA and US.
# pages_controller.rb

  def policy_by_type
    if params[:country].upcase == "CA"
      case params[:policy]
        when "terms"
          redirect_to terms_ca_url
        when "privacy"
          redirect_to privacy_ca_url
        when "electronic_communication"
          redirect_to electronic_communication_ca_url
        else
          render_404
      end
    else
      case params[:policy]
        when "terms"
          redirect_to terms_url
        when "privacy"
          redirect_to privacy_url
        when "electronic_communication"
          redirect_to electronic_communication_url
        else
          render_404
      end
    end
  end

now for constraining policy segment:
# constraints/policy_constraint.rb

class PolicyConstraint
   def self.matches?(request)
     policy_types = ['terms', 'privacy', 'electronic']
     policy_types.include?(request.parameters[:policy])
   end
end

and viola the route is now “constrained” (:

more info: http://guides.rubyonrails.org/routing.html