View Abstraction in Integration Tests

by on

Goal: Make integration tests drier by adding a view abstraction layer

Ruby on Rails has a bunch of popular of test frameworks, such as:

  • RSpec
  • Cucumber
  • Test::Unit
  • Steak

But one common aspect to all of the frameworks, out of the box, is that they're very procedural. Cucumber is designed so that each scenario has a set of steps. There is a single, global, collection of steps. RSpec is global by nature, there are no test classes, just describe blocks.

There is no doubt in my mind that these frameworks have made testing easier by adding lots of common actions, and allowing you to define your own common actions.

However, there is one thing that I don't see much of in tests: Object-Oriented Patterns. Most of us use the Factory pattern through a variety of gems. Internally, many frameworks use the Visitor pattern to execute the tests. But that's all I've ever seen (disclaimer: I am young, and have much to learn).

Here are some pain points I've felt while writing integration tests in the past:

  • Hard to reference objects throughout Cucumber scenarios. Often resulting in global variables used between steps
  • Cumbersome to check the view for expected output, often resulting in lots of css selectors
  • Brittleness introduced by coding html and css structure into test code that prevents refactoring the view

So, I recently had to write some integration tests from scratch, and I decided to do something different. I decided to implement the Bridge Pattern in my integration tests. I was confident enough in the solution that I decided to just use Test::Unit and Capybara to write my test code.

One of the major goals of this implementation is to make it easy to interact with the UI and objects within it. I think it's time I show some code.

Post Test for a simple blog site
# Given I am on the posts page
visit posts_path
# When I create a new post
click_link 'New Post'
View::Post.create(
:title => 'View abstraction in integration tests',
:body => 'We must go deeper'
)
# Then I should see a success message
assert_see 'Successfully created post.'
# When I visit the posts index
visit posts_path
# Then I should see one post
assert_equal 1, View::Post.all.size
# And it should have the correct title
assert_equal 'View abstraction in integration tests',
View::Post.all.first.title
# And it should have the correct body
assert_equal 'We must go deeper', View::Post.all.first.body

Let's take a look at a few interesting things:

  • I'm directly using Capybara's dsl to navigate
  • When I create a post, I'm using a View module so that it won't use the ActiveRecord object
  • When I create a post, I pass in a hash of the fields I'd like to fill in
  • When I check to see if the post is created, I'm using methods on View::Post that return ruby objects, like an array, and that instances of View::Post have methods like "title" and "body"
  • There are no css selectors or html, but I do have button test present

OK, hopefully that piqued your interest. Let's look at some of the implementation. First, let's check out the base View module and a barebones implementation of View::Post:

module View
def self.body
Nokogiri::HTML(Capybara.current_session.body)
end

class Abstract
# Access capybara dsl in the view helper classes too
include Capybara
extend Capybara

def initialize(node)
@id = node['id']
end
def self.all
nodes.map{|node| new(node) }
end
private
def id
%{##{@id}}
end
end
class Post < View::Abstract
attr_reader :title
attr_reader :body
def initialize(node)
super
@title = node.css('.title').first.text.strip
@body = node.css('.body').first.text.strip
end

private
def self.nodes
View.body.css('.post')
end
end
end

OK what's going on here? Let me step you through it:

  • When I call View::Post.all, that calls View::Abstract.all, which iterates over View::Post.nodes and builds instances of View::Post
  • View::Post.nodes runs Capybara's current page through Nokogiri, then selects all the HTML nodes with the class "post"
  • When a View::Post is initialized, it uses Nokogiri to set attributes on itself, from the view. Like title and body
  • View::Abstract always stores an object's dom id as @id so that it can be used internally, which we'll see next.

Now lets take a look at how View::Post.create works:

module View
class Post
def self.create(opts = Hash.new(''))
fill_form opts
click_button 'Create Post'
end
def self.fill_form(opts)
fill_in 'Title', :with => opts[:title]
fill_in 'Body', :with => opts[:body]
end
end
end

Here we show how the class method uses capybara to take care of filling in the form for us. Now, if we change how our forms are rendered, we can change them in one place. Nice and DRY.

Let's look at one of the biggest pain points for me in cucumber: deleting an object in a list of objects. Why is this a pain point? I usually have to write a custom step like "When I delete the Post 'My Post'", which will use dom_id to find the id of a Post object found in the DB with the title "My Post". I find this really roundabout, because you don't need to look in the database for an object to figure out its dom id. It's right there in the view. Any competent internet user would be able to click on the "Delete" button for the post called "My Post" if you showed them the page in a browser. Here is the test code:

# Given I made two blog posts
2.times do |i|
visit posts_path
click_link 'New Post'
View::Post.create(
:title => "Post #{i}",
:body => "Body for #{i}"
)
end
# When I go to the posts path
visit posts_path
# Then I should see two posts
assert_equal 2, View::Post.all.size
# When I delete Post 0
View::Post.find_by_title('Post 0').delete
# Then I should see one post
assert_equal 1, View::Post.all.size
# And I should not see Post 0
assert_nil View::Post.find_by_title('Post 0')
# And I should see Post 1
refute_nil View::Post.find_by_title('Post 1')

Notice specifically the line where the post is deleted. I grabbed the instance of View::Post corresponding to the title I wanted and called .delete on it. Then I thoroughly check that the correct post was removed. Here is the implementation:

module View
class Post
def delete
within(id) { click_button 'Delete' }
end
end
end

Expecting more? In View::Abstract we defined the method "id" which returns the dom id of the object which was stored when we initialized it. I simply told capybara to click "Delete" inside that node's div. This was the "eureka moment" for me. Something that is frustrating and difficult in other styles of testing is just plain simple with this pattern.

There is a lot more than can be done here, and I'm just scratching the surface. If you'd like to try it out for yourself, I've created a Rails project with this environment setup on github: View Abstraction Demo. Here's how to use it:

git clone git://github.com/ngauthier/view-abstraction-demo.git
cd view-abstraction-demo
bundle
rake

Note: you must use ruby 1.9.2. Important files to look at are "test/test_helper.rb" and "test/integration/post_test.rb".

Comments

Jakub
I am sure your idea is gonna turn into a gem. It just fits so well in a view testing pattern and adds performance to it also. I don't fancy Cucumber, use Steak, but still I Repeat Myself with # and . so unql.

Gem would be nice. Generating test/views/abstract.rb and test/views/abstract/model.rb - so nice to modify you know. I'm looking forward to help out with gem development. Wish you luck.
Nick Gauthier
Hey Jakub, glad you liked the post.

I don't think this would make a good gem, because only about 20 lines would be packaged. The generator would create very bare files because it would have no understanding of your css structure.

Also, I've found that UI elements don't always map 1:1 with database elements.

Lastly, I find that the power of this pattern is simple and can be included directly in your code. Putting it in a gem would make it hard to change it on a project-by-project basis. I expect to add more helpful methods to abstract as I use this more.

Thanks,
Nick
Jakub
Thanks for the answer Nick! You can be right, maybe it's too much for a gem, however having that in a generator could be a trick.

There is no need for the gem to understand css nor thoughtlessly map db - it can generate some conventional files that can be easily modified to fit the view. Actually, modifying those files would be a first step of a view specification!

The thing about that pattern is, for me, that those methods should not be stuffed in helpers (or additional spec/test methods) which is usually done. And to not forget that, the gem hooked to a initializer would be nice.
Nick Gauthier
yeah, I think moving them to separate files is definitely a cleaner way of doing it. Feel free to make a gem / plugin with generators to make it easier for yourself.
Corey Haines
Hi, Nick,
Good thoughts.
This looks similar to the page object pattern that a lot of people are using these days in Watir and Selenium tests. Have you looked at that? It could help influence your stuff, hopefully guide you past any dark corners they've already dealt with.
Nick Gauthier
Thanks Corey, that's exactly what I'm trying to do. I figured there was no way I could be the first person to think of this :-)
Nick Gauthier
@Jakub

I've been adding more functionality, and it seems like you're right and it would make a nice gem. Mostly to make it easier to define selectors and attributes, and make a bunch of convenience methods (like the enumerable methods).

Also, I am planning on bundling some assertions with the gem, which will take advantage of capybara's automatic delay on failed assertions. I'll post on this blog soon when it is released.

-Nick
Nick Gauthier
This has been released as a Gem, and the syntax is changed a bit:

https://github.com/ngauthier/domino
blog comments powered by Disqus