Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

« Newer Snippets
Older Snippets »
Showing 1-5 of 5 total  RSS 

RSpec Association Matchers

Ok, what I essentially wanted to accomplish was something like:

describe Product, 'with Group' do

  it 'should belong to group' do
    Product.should belong_to(:product_group)
  end

end

describe ProductGroup, 'with Product' do

  it 'should have many products depending (on group)' do
    ProductGroup.should have_many(:products).depending
  end

end


Here is the code:

module AssociationMatchers

  class AssociationReflection

    def initialize(type, name)
      @messages = {
        :missing_association =>
          '%s is not associated with %s.',
        :wrong_type =>
          "%s %s %s./nExpected: %s",
        :wrong_options =>
          "Options are incorrect.\nExpected: %s Got: %s",
        :missing_column =>
          "Missing foreign key.\nExpected: %s"
      }
      @name = name
      @expected_type = type
      @expected_options = {}
    end
    
    def matches?(target)
      Class === target or
      raise ArgumentError, 'class expected'

      @target = target

      unless @assoc = target.reflect_on_association(@name)
        @failure = :missing_association
        return false
      end 

      unless @assoc.macro.eql?(@expected_type)
        @failure = :wrong_type
        return false
      end

      if @expected_options.any? { |o| @assoc.options[o.first] != o.last }
        @failure = :wrong_options
        return false
      end

      @column ||= @assoc.primary_key_name || @assoc.klass.name.foreign_key

      @failure = case @assoc.macro.to_s
      when 'belongs_to'
        if @target.column_names.include?(@column.to_s) then nil
        else
          :missing_column
        end
      when /(?:has_many|has_one)/
        if    @assoc.options[:through] then nil
        elsif @assoc.klass.column_names.include?(@column.to_s) then nil
        else
          :missing_column
        end
      end

      return @failure.nil?
    end

    def failure_message
      case @failure
      when :missing_association
        @messages[@failure] % [@target.name, @name]
      when :wrong_type
        @messages[@failure] % [
          @target.name,
          @assoc.macro,
          @name,
          @expected_type
        ]
      when :wrong_options
        @messages[@failure] % [
          @expected_options.inspect,
          @assoc.options.inspect
        ]
      when :missing_column
        @messages[@failure] % @column
      end
    end
    def negative_failure_message
    end

    ### Generic Options

    def of(class_name)
      class_name = class_name.name if Class === class_name
      @expected_options[:class_name] = class_name
      self
    end
    def for(foreign_key)
      @column = foreign_key
      self
    end
    def due_to(conditions)
      @expected_options[:conditions] = conditions
      self
    end
    def ordered_by(statement)
      @expected_options[:order] = statement
      self
    end
    def including(*models)
      @expected_options[:include] = (models.length == 1)? models.first: models
      self
    end

  end
  class BelongsToReflection < AssociationReflection

    def initialize(name)
      super :belongs_to, name
    end

    def counted(column)
      @expected_options[:counter_cache] = column
      self
    end
    def polymorphic(true_or_false = true)
      @expected_options[:polymorphic] = true_or_false
      self
    end

  end

  class HasOneReflection < AssociationReflection

    def initialize(name)
      super :has_one, name
    end

    def as(interface_name)
      @expected_options[:as] = interface_name
      self
    end
    def depending(dependency = true)
      @expected_options[:dependent] = dependency
      self
    end
    def extended_by(mod)
      @expected_options[:extend] = mod
      self
    end

  end
  class HasManyReflection < AssociationReflection

    def initialize(name)
      super :has_many, name
    end

    def as(interface_name)
      @expected_options[:as] = interface_name
      self
    end
    def depending(dependency = :destroy)
      @expected_options[:dependent] = dependency
      self
    end

  end
  class HasAndBelongsToManyReflection < AssociationReflection

    def initialize(name)
      super :has_and_belongs_to_many, name
    end

  end

  def belong_to(model)
    BelongsToReflection.new model
  end
  def have_one(model)
    HasOneReflection.new model
  end
  def have_many(models)
    HasManyReflection.new models
  end
  def have_and_belong_to_many(models)
    HasAndBelongsToManyReflection.new models
  end
  alias_method :habtm, :have_and_belong_to_many

end


It checks:
* association exists
* association macro
* foreign key exists (except for habtm)
* options match (only a subset is supported)

Setup:
* put the code in RAILS_ROOT + "/lib/association_matchers.rb"
* put "config.include AssociationMatchers # lib/association_matchers.rb" in your spec_helper.rb configure block
* refactor your model specs...

RSpec extension: testing attr_protected and attr_accessible

Include this module in a spec or inside the Spec::Runner.configure block in the spec_helper.rb. Because i suck, should_not is not working as expected. Usage:

class User < ActiveRecord::Base
  # either 
  attr_accessible :username, :full_name
  # or
  attr_protected :admin, :foo
end

# ...

it "should protect admin and foo" do
  @user.should protect_attributes(:admin, :foo)
end


Here goes:

module CustomExpectations
  class ProtectAttributes
    def initialize(*attributes)
      @attributes = attributes
    end
    
    def matches?(target)
      @target = target
      
      calculate_protected_methods
      perform_check
    end
    
    def failure_message
      "expected #{@failed_attribute} to be protected"
    end
    
    def negative_failure_message
      "expected #{@failed_attribute} to not be protected"
    end
    
    private
    
    def calculate_protected_methods
      read = proc {|var| @target.instance_eval { self.class.read_inheritable_attribute(var) } }
      accessible = read.call("attr_accessible")
      protekted = read.call("attr_protected")
      all = @target.class.column_names.map(&:to_sym)
      
      @protected = []
      @protected << protekted if protekted
      @protected << (all - accessible) if accessible
      @protected.flatten!
      
      @accessible = all - @protected
    end
    
    def perform_check
      failed_attributes = (@attributes & @accessible)
      @failed_attribute = failed_attributes.first
      failed_attributes.empty?
    end
  end
  
  def protect_attributes(attributes)
    ProtectAttributes.new(attributes)
  end
  
  def protect_attribute(attribute)
    ProtectAttributes.new(*[attribute])
  end
end

Rspec my validations for models

// This is the plain-vanilla way that I've been using spec for basic model validations.
// There is also a bit at the bottom that checks that I have the right association
// setup.

require File.dirname(__FILE__) + '/../spec_helper'

module CommentSpecHelper
  def valid_comment_attributes
    { :body => 'Some text',
      :commentable_type => 'School',
      :commentable_id => 1 }
  end
end

describe Comment do

  include CommentSpecHelper

  before(:each) do
    @comment = Comment.new
  end

  it "should be valid" do
    @comment.attributes = valid_comment_attributes
    @comment.should be_valid
  end
  
  it "should should not be valid without something to attach to" do
    c = valid_comment_attributes
    c.delete :commentable_id
    c.delete :commentable_type
    @comment.attributes = c
    @comment.should_not be_valid
    @comment.errors.on(:commentable_id).should eql("can't be blank")
    @comment.commentable_id = 1
    @comment.should_not be_valid
    @comment.errors.on(:commentable_type).should eql("can't be blank")
    @comment.commentable_type = "School"
    @comment.should be_valid
  end
  
  it "should require body" do
    @comment.attributes = valid_comment_attributes.except(:body)
    @comment.should_not be_valid
    @comment.errors.on(:body).should eql("can't be blank")
    @comment.body = "Some text"
    @comment.should be_valid
  end

  it "should relate to commentable" do
    Comment.reflect_on_association(:commentable).should_not be_nil
  end
  
  it "should relate to user" do
    Comment.reflect_on_association(:user).should_not be_nil
  end
end

vim folding for RSpec editing


" fold text between context and specify
function! ShowRSpecContext()
  let spec_idx = search('specify\s\+".\+"', 'Wbn', '^')
  let ctx_idx  = search('context\s\+".\+"', 'Wbn', '^')
  if spec_idx && ctx_idx
    exec (ctx_idx+1).','.(spec_idx-1).'fold'
  endif
endfunction

" fold text between all contexts and specify lines
function! ShowRSpecAnnotation()
 call cursor('$', 0)
 try
   foldo!
 catch
 endtry
 let cur_line = line('$')
 while cur_line > 0
   let prev_spec = search('\(context\|specify\)\s\+".\+"', 'Wb', '^')
   if ! prev_spec
     break
   endif
   exec (prev_spec).','.cur_line.'fold'
   let cur_line=prev_spec-1
 endwhile
endfunction
command! Sx :call ShowRSpecContext()
command! Sa :call ShowRSpecAnnotation()

Extending 'context' with new methods in RSpec

Define a new method like this:

module Spec::Runner::ContextEval::ModuleMethods
  def new_method
    # ...
  end
end


Then use like that:

context "Some state" do
  new_method
end
« Newer Snippets
Older Snippets »
Showing 1-5 of 5 total  RSS