giant robots smashing into other giant robots

Written by thoughtbot

dancroak

Factory Girl callbacks

Factory Girl now has callbacks thanks to Nate Sutton.

this shit is bananas

There are three callbacks:

after_build
after_create
after_stub

When build(:user) is invoked, the after_build callback runs after building the user. Likewise for create and stub.

These come in handy in a number of common use cases.

Basic has many associations

Models:

class Article < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :article
end

Factories:

factory :article do
  body 'password'

  factory :article_with_comment do
    after_create do |article|
      create(:comment, article: article)
    end
  end
end

factory :comment do
  body 'Great article!'
end

Nice. Callbacks let us do this:

article = create(:article_with_comment)

Instead of this:

article = create(:article)
create(:comment, article: article)

Polymorphic relationships

The savings get larger when the object graph gets more complex:

Models:

class User < ActiveRecord::Base
  has_many :interests, as: :interested
  has_many :topics, through: :interests
end

class Interest < ActiveRecord::Base
  belongs_to :topic
  belongs_to :interested, polymorphic: true
end

Building block factories:

factory :user
  email
  password 'password'

  factory :email_confirmed_user do
    email_confirmed { true }
  end
end

factory :topic do
  name 'topic_name'
end

factory :interest
  topic
  interested factory: :user
end

factory :music_interest, class: 'Interest' do
  topic.association(:topic, name: 'Music')
end

factory :sports_interest, class: 'Interest' do
  topic.association(:topic, name: 'Sports')
end

And now the payoff:

factory :musical_user, parent: :email_confirmed_user do
  after_create { |user| create(:music_interest, interested: user)
end

factory :sporty_user, parent: :email_confirmed_user do
  after_create { |user| create(:sports_interest, interested: user) }
end

Again, factories let us do this:

user = create(:musical_user)

Instead of:

user = create(:email_confirmed_user)
create(:music_interest, interested: user)

More intention-revealing. More encapsulation, protecting us from change.

Working with fakes

Fakes are a good approach for testing objects that interface with web services such as geocoding and payment processing.

class User < ActiveRecord::Base
  acts_as_mappable
  before_validation :geocode_location, if: :location_changed?

  def geocode_location
    geo = Geokit::Geocoders::MultiGeocoder.geocode(location)
    self.lat, self.lng = geo.lat, geo.lng
  end
end

factory :user do
  factory :boston_user do
    location 'Boston, MA'

    after_build do |user|
      Geokit::Geocoders::FakeGeocoder.locations['Boston, MA'] = [0, 1]
    end
  end
end

Enjoy!

Install:

gem 'factory_girl'

Happy testing.

Written by .

dancroak

When, a Rails plugin

Ever write this?
before_filter :authorize

protected

def authorize
  unless logged_in?
    session[:return_to] = request.request_uri
    redirect_to login_url and return false
  end
end
You want your before_filter to authorize the current_user unless they are logged in. So why not say it like you mean it?
before_filter :authorize, :unless => logged_in?

protected

def authorize
  session[:return_to] = request.request_uri
  redirect_to login_url and return false
end

Paris Hilton is a fan of moving conditional logic into filters, callbacks, and validations.This is more expressive. The conditional logic is no longer hidden in the method.

The authorize method now has a single responsibility: to authorize. The before_filter, responsible for performing some action before your controller action is called, is now smarter about when it is supposed to run.


Introducing When

The When Rails plugin adds :if and :unless modifiers to before_filters, most ActiveRecord callbacks, and validations.

Get it:

piston import https://svn.thoughtbot.com/plugins/when/trunk vendor/plugins/when
Then start improving your controllers:
before_filter :deny_access, :unless => :admin?

protected

def deny_access
  flash[:failure] = "You do not have access to that page."
  redirect_to home_url
end

def admin?
  logged_in? and current_user.admin?
end
Improve your models, too:
before_create :encrypt_password,
  :unless => lambda {|user| user.password_confirmation.blank?}

after_save :send_alerts, :if => :alerts?

validate_on_create :add_unsupported_type_error, 
  :unless => lambda {|document| SUPPORTED_FILE_TYPES.include? document.file_type}

Coding without ifs?

Jared makes a strong argument that this is actually removing conditional logic from your program. I tend to agree.

It reminds me of defining constants in your environments so you don’t write conditional logic that checks the RAILS_ENV. That kind of control flow belongs in the framework.

Whether this is removing conditional logic is moot, though. It just feels right, and isn’t that why we write Ruby?

UpdateAfter releasing this, we found out this is already in Rails trunk. Awesome! It’s a great feature and should be in the framework. So, if you’re not running on Edge, use the when plugin to get this feature now. Then, remove it when you upgrade to the next version of Rails, when you’ll get :ifs and :unlesses “for free.” Happy coding!