SuperPumpup (dot com)
General awesomeness may be found here.

03 March 2013

Leveling up in RSpec - DRY

Repetitive tests suck. Just like repetitive code sucks. Repetitive tests are a pain to maintain so are likely to get stale. I very clearly know that repetitive code is To Be Avoided (and tools like CodeClimate make repetitive code glaring to me), and so don't do it. However, I remember being SO PROUD of my first text editor extension (a Sublime Text 2 Snippet):

it "requires a <property>" do
   FactoryGirl.build(<class_name>, <property>: nil).should_not be_valid
 end

Because somehow generating a ton of identical test code to test built-in AR functionality was a good idea. In retrospect it's a bit horrific, but hey, it made it SO EASY to get a very high test LOC:'real' LOC ratio, and got me excellent RCov results. Yaay for me.

But of course those rae not super-useful tests and so here I'll jot down a few of the ways that I've tried to make a block of test code reusable, as well as go discover some (maybe better) ones.

1. Test A Bunch Of Objects In The Same Test

This is probably the least elegant (worst?) solution I've employed and it winds up looking like this:

describe "Attachment uploads" do
  context "File attachments" do
    it "user can attach things to other things" do
      default_url_options[:host] = "127.0.0.1:6543" # This is important later
      sign_in(:executive)
      objects = [
        create(:job),
        create(:employee),
        # and any other objects you'd like to test this on
      ]
      objects.each do |object|
        visit url_for(object) # Need to have set the default_url_options above for this to work usually
        # Do your attachment
        page.should #have your attachment
      end
    end
  end
end

I feel like this is a good solution to this in something with TONS of overhead like Capybara/Selenium, but breaks down at lower levels. There is a problem that an assertion on one of the objects doesn't give you obvious output as to which object it was testing when it failed, so typically it involves a bit more digging.

2. Use it_behaves_like To Ensure An Object Fulfills A Role

This is a pretty awesome technique that I found reading Practical Object Oriented Design in Ruby and wrote about before this. I won't repeat myself, but it's a really good I think.

One thing that I don't like about it is that I have to include the it_behaves_like in each of the specs that you're executing. I don't like that as much as I prefer having them centralized in one place, so let's see if this can be "improved" somewhat.

3. Centralize Your Interface Assertions

Turns out it looks ilke it can!

#spec/models/attachable_spec.rb
require 'spec_helper'

shared_examples_for "Attachable" do
  describe "implements attachable" do
    let(:model) { described_class.new() }
    it "can reference its attachments" do
      model.attachments.should == []
    end
  end
end

describe User do
  it_behaves_like "Attachable"
end

describe Task do
  it_behaves_like "Attachable"
end

I'm torn about this - not sure if I like it better or worse, but maybe I'll use it in the future.

4. "Metaprogram" Your Tests

I'll try another something...

In Cucumber there is a slick way of saying something like:

Given the following user users
  | username | password | admin |
  | bob      | secret   | false |
  | admin    | secret   | true  |
Given I am logged in as "<login>" with password "secret"
When I visit profile for "<profile>"
Then I should <action>

Examples:
  | login | profile | action                 |
  | admin | bob     | see "Edit Profile"     |
  | bob   | bob     | see "Edit Profile"     |
  |       | bob     | not see "Edit Profile" |
  | bob   | admin   | not see "Edit Profile" |

Example borrowed from here

And it turns out in RSpec you can do something similar. So if I have some model with a bunch of associations (both member and collection) and I want to test that they are always nil or empty. You can do this:

describe "associations" do
  let(:job) { Job.new }
  collections = [:timesheets, :purchase_orders, :fuelings, :audits]

  collections.each do |collection|
    it "returns an empty array for the collection #{collection}" do
      job.send(collection).should == []
    end
  end

  members = [:customer, :supervisor, :job_site]

  members.each do |member|
    it "returns nil for the #{member} if unassigned" do
      job.send(member).should == nil
    end

    it "can have a factory-built #{member} assigned" do
      factory = FactoryGirl.build(member)
      job.send("#{member.to_s}=", factory)
      job.send(member).should == factory
    end
  end
end

Not too bad! I'm happy, I learned some new RSpec tricks (and wound up actually improving my test suites as I was experimenting) and hopefully shared something with you!

Do you have any good tricks for keeping your specs DRY? Let me know on Twitter!

Categories: RSpec Ruby on Rails