giant robots smashing into other giant robots

We are thoughtbot. We make web & mobile apps.

Tagged:

Comments (View)

FactoryGirl 2.1.0 Brings the Heat

Factory Girl has seen a handful of changes over the past six weeks since we released 2.0.0. Some of the highlights:

Simple association syntax

factory :user do
  name "John doe"
end

factory :post do
  author :factory => :user
end

Traits

trait :male do
  gender "Male"
end

trait :admin do
  admin true
end

factory :user do
  factory :male_user,       :traits => [:male]
  factory :admin,           :traits => [:admin]
  factory :male_admin_user, :traits => [:male, :admin]
end

Transient attributes

factory :user do
  rockstar(true).ignore
  name { "Johnny#{" Rockstar" if rockstar}" }
end

> FactoryGirl.create(:user).name                     # "Johnny Rockstar"
> FactoryGirl.create(:user, :rockstar => false).name # "Johnny"

Change associations to build instead of create

factory :profile do
  sequence(:username) {|n| "user-#{n}" }
end

factory :user do
  profile :method => :build
end

Factory modification

FactoryGirl.define do
  factory :user do
    name "John Doe"
    sequence(:email) {|n| "user-#{n}@example.com" }
  end
end

FactoryGirl.modify do
  factory :user do
    email { "#{name.downcase.underscore}@example.com" }
  end
end

Factory reloading (handy in a console)

FactoryGirl.reload # reloads all factories, sequences, and traits

Apart from these features, we’ve ensured that Factory Girl processes different attributes (static attributes, dynamic attributes) in a reasonable order, verified it works on Rails 3.1.0, upgraded the test suite to use Mocha + Bourne instead of RR (we at Thoughtbot love Mocha), and a handful of other handy bug fixes.

Grab Factory Girl 2.1.0 and make testing easier!

Tagged:

Comments (View)

Never Fear, Traits are Here

I can’t count the number of times I’ve worked on Rails apps where I’m using Factory Girl and want to declare a factory with multiple parents. We’ve all been there, don’t lie.

FactoryGirl.define do
  factory :user do
    name "Friendly User"

    factory :admin do
      admin true
    end

    factory :male_user do
      gender "Male"
      name   "John Doe"

      factory :male_admin_user do
        admin true
      end

      factory :teenage_male_user do
        date_of_birth { 15.years.ago }

        factory :teenage_admin_male_user do
          admin true
        end
      end
    end

    factory :female_user do
      gender "Female"
      name   "Jane Doe"

      factory :female_admin_user do
        admin true
      end

      factory :teenage_female_user do
        date_of_birth { 15.years.ago }

        factory :teenage_admin_female_user do
          admin true
        end
      end
    end
  end
end

Oh, the duplication! This isn’t DRY.

Factory Girl 2.0.4 introduces traits. I’ll cut to the chase:

FactoryGirl.define do
  factory :user do
    name "Friendly User"

    trait :admin do
      admin true
    end

    trait :male do
      gender "Male"
      name   "John Doe"
    end

    trait :female do
      gender "Female"
      name   "Jane Doe"
    end

    trait :teenager do
      date_of_birth { 15.years.ago }
    end

    factory :male_user,                 :traits => [:male]
    factory :female_user,               :traits => [:female]
    factory :teenage_male_user,         :traits => [:teenager, :male]
    factory :teenage_female_user,       :traits => [:teenager, :female]
    factory :male_admin_user,           :traits => [:admin, :male]
    factory :female_admin_user,         :traits => [:admin, :female]
    factory :teenage_admin_male_user,   :traits => [:teenager, :admin, :male]
    factory :teenage_admin_female_user, :traits => [:teenager, :admin, :female]
  end
end

Mix and match however you please. Bonus: you can assign traits like plain-old attributes.

FactoryGirl.define do
  factory :user do
    name "Friendly User"

    trait :admin do
      admin true
    end

    trait :male do
      gender "Male"
      name   "John Doe"
    end

    factory :male_admin_user do
      male
      admin
    end
  end
end

Many props go out to Thomas Walpole, who helped get a lot of this written; thanks Thomas!

So go ahead, get your trait on; you’ll thank me later.

Tagged:

Comments (View)

Testing Paperclip on S3 with Cucumber & Factory Girl

We’ve been using Heroku as a staging environment for our latest project. One constraint is a read-only filesystem.

The most apparent effect of this is we cannot allow users to upload files to the filesystem.

Fine. Paperclip has an S3 storage option.

Testing

Webrat has a very nice existing convention for interacting with file fields:

When /^I attach the file at "([^\"]*)" to "([^\"]*)"$/ do |path, field|
  attach_file(field, path)
end

Unfortunately, if we have an S3-backed model like this…

has_attached_file :logo,
 :path           => ":attachment/:id/:style.:extension",
 :storage        => :s3,
 :s3_credentials => {
   :access_key_id     => ENV['S3_KEY'],
   :secret_access_key => ENV['S3_SECRET']
 },
 :bucket         => ENV['S3_BUCKET']

… then we’re going to be doing a RESTful PUT to S3 during each test run.

Incidentally, those ENV variables are Heroku’s config vars. The idea is that you keep that configuration separated from your source control.

Total integration?

You could argue that these PUTs to S3 are a good thing because your Cucumber feature will represent total integration.

While that’s true, I’d rather not pay bandwidth costs and I’m comfortable as long as the correct interface to S3 was called.

So what we’ve landed on is something like this:

Given I am on the new band page
When I attach a "demo_tape" "mp3" file to a "band" on S3
And I press "Upload demo tape"
Then I should see "Band was successfully created"

The only non-standard Webrat step is our new S3 step. Let’s take a look at it:

# features/step_definitions/paperclip_steps.rb

When /^I attach an? "([^\"]*)" "([^\"]*)" file to an? "([^\"]*)" on S3$/ do |attachment, extension, model|
  stub_paperclip_s3(model, attachment, extension)
  attach_file attachment,
              "features/support/paperclip/#{model.gsub(" ", "_").underscore}/#{attachment}.#{extension}"
end

The stub_paperclip_s3 method is coming from a custom Shoulda Macro:

# test/shoulda_macros/paperclip.rb

module Paperclip
  module Shoulda
    def stub_paperclip_s3(model, attachment, extension)
      definition = model.gsub(" ", "_").classify.constantize.
                         attachment_definitions[attachment.to_sym]

      path = "http://s3.amazonaws.com/:id/#{definition[:path]}"
      path.gsub!(/:([^\/\.]+)/) do |match|
        "([^\/\.]+)"
      end

      FakeWeb.register_uri(:put, Regexp.new(path), :body => "OK")
    end

    def paperclip_fixture(model, attachment, extension)
      stub_paperclip_s3(model, attachment, extension)
      base_path = File.join(File.dirname(__FILE__), "..", "..",
                            "features", "support", "paperclip")
      File.new(File.join(base_path, model, "#{attachment}.#{extension}"))
    end
  end
end

class ActionController::Integration::Session
  include Paperclip::Shoulda
end

class Factory
  include Paperclip::Shoulda
end

Fakeweb and more conventions

We’re using the Fakeweb gem like we normally use mocking: expect that something happened, and stop it from actually happening.

We’re also leaning on conventions similar to the actor directory convention we’re also trying.

In this case, we’re expecting our features directory to look like this:

features/support/paperclip/band/demo_tape.mp3
features/support/paperclip/band/demo_tape.aac
features/support/paperclip/band/demo_tape.ogg
features/support/paperclip/band/demo_tape.wav
features/support/paperclip/user/avatar.png
features/support/paperclip/user/avatar.jpg
features/support/paperclip/user/avatar.gif

This allows us to test the expected and unexpected formats by changing this line:

When I attach a "demo_tape" "mp3" file to a "band" on S3

Factories

The reason the stub_paperclip_s3 and paperclip_fixture methods are set up as a custom shoulda macro is so that you can use them in your factory code:

Factory.define :band_with_demo_tape, :parent => :band do |band|
  band.demo_tape { band.paperclip_fixture("band", "demo_tape", "png") }
end

Go or no go?

It’s been quite useful so far for us. How are you testing Paperclip uploads to S3? Do you see any way to improve this step definition or the convention?