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:
- Delegate filling in the form to a reusable helper
- Make the assertion in helper
- 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:
- I didn't want tons of flat helpers (i.e. create_blog, create_comment, create_user)
- 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:
- Wait a minute, he said more simple! How is introducing some new framework going to make this simpler?
- 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: