There lots of advocacy against integration testing out there because over time they become too difficult to maintain and are hard to move with the application. Tests eventually become a burden and teams leave them behind, preferring to go without any sort of safety net to spending time updating capybara calls in 100 tests.

The root of the problem here isn't that integration testing is bad, wasteful or particularly hard, it's that we left all our good ideas about coding in the production codebase when it came time to make our tests.

Spaghetti Definitions

Take this basic step definition for an example

Given(/^I am logged in$/) do
  @user = create(:user)
  click_link "Sign In"
  fill_in "#email", with: @user.email
  fill_in "#password", with: @user.password
  click_button "Sign In"
end

It works! Then you run bundle update devise at some point and suddenly this breaks everywhere because they decided "Log In" was less likely to be confused with "Sign Up". Or maybe someone decides that you should be able to sign in with username or email, and the id of the email field changes to "#user_identifier" or something.

Not a big deal the first few times, but as your app grows in size and complexity, it becomes difficult or impossible to keep your tests up to date with changes. You'll start excluded things from the CI build, deleting tests and eventually just manually testing and calling it good.

Page Objects are your friends

The problem with using capybara or watir-webdriver statements directly in your step definitions is it breaks encapsulation. You're making calls in the global cucumber namespace to implementation details. It may be easier to get going, but make no mistake, it's technical debt.

# features/support/pages/sign_in_page.rb
class SignInPage
  include Capybara::DSL
  def email_field
    find("input[id='email']")
  end
  def password_field
    find("input[id='password']")
  end
  def sign_in_button
    find("button[value='Sign In']")
  end

  def sign_in_as(user)
    email_field.set user.email
    password_field.set user.password
    sign_in_button.click
  end
end

# features/step_definitions/authentication_steps.rb
Given(/^I am logged in$/) do
  SignInPage.new.sign_in_as(create(:user))
end

This is a super simple implementation of a page object. The object now is an interface between your test and the actual browser, and your test code just sends it a message on what to do. If a detail changes, you just change the class and the sign_in_as message will continue to do the work of signing in.

Page Object Libraries

In the context of a Rails project with Capybara as the integration testing driver, Site Prism is really the only game in town. It provides a very simple DSL that will let you build up page objects without a lot of boilerplate methods for creating element accessors.

# features/support/pages/sign_in_page.rb
class SignInPage < SitePrism::Page
  element :email_field, "input[id='email']"
  element :password_field, "input[id='password']"
  element :sign_in_button, "button[value='Sign In']"

  def sign_in_as(user)
    email_field.set user.email
    password_field.set user.password
    sign_in_button.click
  end
end

As you can see, you get Capybara::DSL automatically, and you can use the element class method to define you element finders, resulting in a much cleaner class. It has loads of other features, so be sure to checkout their github page.

Method Chaining

This is great, but sometimes you'll want to do multiple operations on each page. Sometimes it will read nicer to do each of these on their own line, sometime it will be nicer to chain methods.

class PostPage < SitePrism::Page
  set_url "/posts{/id}"
  element :reply, "a[id='reply']"
  element :quick_add_comment_body, "#comment_body"
  element :quick_add_comment_submit, "button[value='Add Comment']"
  element :comment_count, "span[id='comment_count']"

  def reply_to_post
    reply.click
  end

  def quick_add_comment(text="WAT")
    quick_add_comment_body.set text
    quick_add_comment_submit.click
  end
end

Here we have a page object that wraps a reply link and a quick comment form. The reply link will show the quick-reply form. This could be one operation, but we'll show it as two separate steps.

When(/^I quick add a comment$/) do
  # assume @post is already populated in then given step
  post_page = PostPage.new.load(id: @post.id)
  post_page.reply
  post_page.quick_add_comment "Hello World"
end

In this case, since it's one sequence of operations and the methods read cleanly, method chaining would be nice. This is just a matter of returning self from the page object

class PostPage < SitePrism::Page
  def reply_to_post
    reply.click
    self
  end
end

When(/^I quick add a comment$/) do
  PostPage.new.load(id: @post.id).reply.quick_add_comment "Hello World"
end

Generally, I have the methods return self if the user is going to remain on the current page. If they will navigate to another page, I return an instance of that page.

def sign_in_as(user)
  # sign_in code
  return HomePage.new
end

An app class for managing your page instances

As a final tip, as suggested by the site prism wiki, having a class that's responsible for managing your page object instances helps clean up your tests a lot.

# features/support/pages/app.rb
class App
  def home_page
    HomePage.new  
  end
end

# features/support/hooks.rb
Before do
  @app = App.new
end

# features/step_definitions/home_steps.rb
Given(/^I am on the home page$/) do
  @app.home_page.load
end

This also opens up some doors for meta-programming. Personally I don't like defining methods for each page object, but like the accessibility. So I implement something like this:

class App
  # Page Object Accessors
  # Converts a method call to a page object class and establishes a new instance
  # @app.home_page    # => HomePage
  # @app.sign_in_page # => SignInPage
  def method_missing(name)
    klass = name.to_s.camelize
    create_page_object_or_raise_error(klass)
  end

private

  def constant_exists?(klass_string)
    Object.const_defined? klass_string
  end

  def create_page_object_or_raise_error(klass_string)
    if constant_exists? klass_string
      return klass_string.constantize.new
    else
      raise StandardError, "There's no page object currently defined called #{klass_string}. Create one under features/support/pages"
    end
  end
end

This lets me use the same interface of @app.page_klass_name, but I don't have to define methods for each one, and it will also fail a test if I try to use a page object I haven't defined yet.

That's all for this introductory post on Object Oriented Testing. Next time we'll cover Test Services