TestPilot - Rails Development Integration Testing Pattern

I've been thinking about ways to simplify rails integration testing. I wanted to see how well I could do it without cucumber and rspec. I decided to go pure minitest with capybara to help out. What emerged was the TestPilot pattern. Let's check it out.

Note: all of this code is runnable via the test-pilot-demo on github.

Testing the Index

This one is super simple. It's just minitest and capybara:

test "See the index" do  
    visit root_path
    assert_see "Blog Posts"
  end

You'll notice I threw in "assert_see". It's a simple method that wraps some capybara code:

def assert_see(content)  
    assert(page.has_content?(content), "Expected page to contain \"#{content}\", but it didn't")
  end

Testing Create

Now we're getting into some real data business. These were my goals:

  1. Delegate filling in the form to a reusable helper
  2. Make the assertion in helper
  3. Deal with models like models, not like attribute hashes

Here is my test code:

test "Create a new post" do  
    BlogPilot do
      create(
        Blog.new(:title => 'a blog title', :body => 'a blog body')
      )
    end
  end

I delegating filling out the form to BlogPilot#create_blog. I also did it by passing in a blog object to be created. You'll note there are no assertions. This was a personal choice. I wanted to write out my steps such that they are assumed to succeed. Any step that fails will fail the test.

So, what's with this pilot business? Let's check it out:

class BlogPilot < TestPilot::Core  
  def create(blog)
    visit root_path
    fill_in "Title", :with => blog.title
    fill_in "Body", :with => blog.body
    click_button 'Create Post'
    assert_on root_path
    assert_see blog.title
    assert_see blog.body
  end
end  

That's pretty straightforward! It's just a capybara helper. You'll note the step makes assertions. The only way the test can proceed is if this step passes. That way, we've also abstracted the notion of "successfully creating a blog" to a single place. So if we switch between divs, tables, and lists to display our blog, we only change our expectations here.

So what's this TestPilot::Core business? There are two reasons I had to make TestPilot:

  1. I didn't want tons of flat helpers (i.e. create_blog, create_comment, create_user)
  2. I didn't want to have to instantiate the pilot and keep calling it. I wanted "BlogPilot do ... end"

TestPilot

Two things should be going through your head right now:

  1. Wait a minute, he said more simple! How is introducing some new framework going to make this simpler?
  2. HOW DO I GET THE GOODNESS IN ME

Luckily, the answer to both questions is the same. This is test pilot:

module TestPilot  
  class Core < ActionDispatch::IntegrationTest
    def initialize ; end # override new from inherited class
    def self.inherited(subclass)
      TestPilot::Dsl.send(:define_method, subclass.to_s) do |&block|
        subclass.new.instance_eval(&block)
      end
    end
  end
  module Dsl ; end
end  

It's not a gem. It barely deserves to be in lib. This could go right in your test_helper.rb. What's going to break? There are two lines of real code in there. And if it breaks, guess what? It's right there in test_helper, not in some gem you have to maintain your own fork of so you can have "Nick's raspberry flavored test::pilot".

Where am I going?

So what was my goal with this? My goal is to write code like this all day:

test "a logged in user can comment on another user's post" do  
  joe = Factory :user
  bob = Factory :user
  UserPilot do
    log_in joe
  end
  PostPilot do
    @post = create(Factory.build(:post))
  end
  UserPilot do
    log_out
    log_in bob
  end
  CommentPilot do
    create(@post, Factory.build(:comment))
  end
end  

Notice I left out any assertions. I think it would be cool to have the pilot steps themselves assert a successful action, since you probably want to check for it every time anyways.

I enjoy that this gives me one of my favorite benefits of cucumber: reusable steps. But, I can carry around instance variables in a highly visible manner, and call specialized factories without creating a one-off step that obfuscates my codebase.

So what do you think? Would you rather code cucumber, rspec, raw test unit, test pilot? Or have I inspired you to roll your own?

Code: test-pilot-demo.

Check out our other articles on rails development:

How to Test PDF Content with Capybara

Find the Unique Sessions for a Rails Application

comments powered by Disqus