Skip to content

Instantly share code, notes, and snippets.

@tourdedave
Created February 25, 2014 20:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tourdedave/9217122 to your computer and use it in GitHub Desktop.
Save tourdedave/9217122 to your computer and use it in GitHub Desktop.

How To Reuse Your Test Code

One of the biggest challenges with Selenium tests is that they can be brittle and challenging to maintain over time. This is largely due to the fact that things in the app you're testing change, breaking your tests.

But the reality of a software project is that change is a constant. So we need to account for this reality somehow in our test code in order to be successful.

Enter Page Objects.

Rather than write your test code directly against your app, you can model the behavior of your application into simple objects -- and write your tests against them instead. That way when your app changes and your tests break, you only have to update your test code in one place to fix it.

And with this approach, we not only get the benefit of controlled chaos, we also get the benefit of reusable functionality across our tests.

Let's take our previous login example, pull it out into a page object, and update our test accordingly.

An Example

Part 1: Create A Page Object

# filename: login.rb

class Login

  LOGIN_FORM  = { id: 'login' }
    USERNAME_INPUT = { id: 'username' }
    PASSWORD_INPUT = { id: 'password' }
  SUCCESS_MESSAGE = { css: '.flash.success' }

  def initialize(driver)
    @driver = driver
    @driver.get ENV['base_url'] + '/login'
  end

  def with(username, password)
    @driver.find_element(USERNAME_INPUT).send_keys(username)
    @driver.find_element(PASSWORD_INPUT).send_keys(password)
    @driver.find_element(LOGIN_FORM).submit
  end

  def success_message_present?
    @driver.find_element(SUCCESS_MESSAGE).displayed?
  end

end

We start by creating our own class, naming it Login, and storing our locators along the top. We then use an initializer to receive the Selenium driver object and visit the login page.

In our with method we are capturing the core functionality of the login page by accepting the username and password input values as arguments and housing the input and submit actions.

Since our behavior now lives in a page object, we want a clean way to make an assertion in our test. This is where success_message_present? comes in. Notice that it ends with a question mark. In Ruby, when methods end with a question mark, they imply that they will return a boolean value (e.g., true or false).

This enables us to ask a question of the page, receive a boolean response, and make an assertion against it.

Part 2: Update The Login Test

# filename: login_spec.rb

require 'selenium-webdriver'
require_relative 'login'

describe 'Login' do

  before(:each) do
    @driver = Selenium::WebDriver.for :firefox
    ENV['base_url'] = 'http://the-internet.herokuapp.com'
    @login = Login.new(@driver)
  end

  after(:each) do
    @driver.quit
  end

  it 'succeeded' do
    @login.with('tomsmith', 'SuperSecretPassword!')
    @login.success_message_present?.should be_true
  end

end

At the top of the file we include the page object with require_relative (this enables us to reference another file based on the current file's path). We then create an environment variable for the base_url (ENV['base_url']). Since we only have one test file, it will live here and be hard-coded for now. But don't worry, we'll address this in a future post.

Next we instantiate our login page object in before(:each), passing in @driver as an argument, and storing it in an instance variable (@login). We then modify our 'succeeded' test to use @login and it's available actions.

Part 3: Write Another Test

This may feel like more work than what we had when we first started. But we're in a much sturdier position and able to write follow-on tests more easily. Let's add another test to demonstrate a failed login.

If we provide incorrect credentials, the following markup gets rendered on the page.

<div id="flash-messages" class="large-12 columns">
  <div data-alert="" id="flash" class="flash error">
    Your username is invalid!
    <a href="#" class="close">x</a>
  </div>
</div>

This is similar to the markup from the successful flash message, so let's mimic the behavior we used in our page object to create another method to help in our assertion.

First we'll add a new locator for the failure message in our list of locators at the top of our class (just below our success message locator).

# filename: login.rb

class Login

  LOGIN_FORM  = { id: 'login' }
    USERNAME_INPUT = { id: 'username' }
    PASSWORD_INPUT = { id: 'password' }
  SUCCESS_MESSAGE = { css: '.flash.success' }
  FAILURE_MESSAGE = { css: '.flash.error' }
  ...

Further down the file (next to the existing display check method) we'll add a new method to check for the existence of this message and return a boolean response.

def success_message_present?
  driver.find_element(:css, SUCCESS_MESSAGE).displayed?
end

def failure_message_present?
  driver.find_element(:css, FAILURE_MESSAGE).displayed?
end

Lastly, we add a new test in our spec file just below our existing one, specifying invalid credentials to force a failure.

it 'succeeded' do
  @login.with('tomsmith', 'SuperSecretPassword!')
  @login.success_message_present?.should be_true
end

it 'failed' do
  @login.with('asdf', 'asdf')
  @login.failure_message_present?.should be_true
end

Now if we run our spec file (rspec login_spec.rb) we will see two browser windows open (one after the other) testing both the successful and failure login conditions.

Why Asserting False Won't Work (yet)

You may be wondering why we didn't check to see if the success message wasn't present.

it 'failed' do
  @login.with('tomsmith', 'SuperSecretPassword!')
  @login.success_message_present?.should be_false
end

There are two problems with this approach. First, our test will fail because it errors out when looking looking for an element that's not present -- which looks like this:

.F

Failures:

  1) Login failed
     Failure/Error: @login.success_message_present?.should be_false
     Selenium::WebDriver::Error::NoSuchElementError:
       Unable to locate element: {"method":"css selector","selector":".flash.success"}
     # [remote server] file:///tmp/webdriver-profile20140125-21003-3brprw/extensions/fxdriver@googlecode.com/components/driver_component.js:8860:in `FirefoxDriver.prototype.findElementInternal_'
     # [remote server] file:///tmp/webdriver-profile20140125-21003-3brprw/extensions/fxdriver@googlecode.com/components/driver_component.js:8869:in `FirefoxDriver.prototype.findElement'
     # [remote server] file:///tmp/webdriver-profile20140125-21003-3brprw/extensions/fxdriver@googlecode.com/components/command_processor.js:10831:in `DelayedCommand.prototype.executeInternal_/h'
     # [remote server] file:///tmp/webdriver-profile20140125-21003-3brprw/extensions/fxdriver@googlecode.com/components/command_processor.js:10836:in `DelayedCommand.prototype.executeInternal_'
     # [remote server] file:///tmp/webdriver-profile20140125-21003-3brprw/extensions/fxdriver@googlecode.com/components/command_processor.js:10778:in `DelayedCommand.prototype.execute/<'
     # ./code_examples/07/02/login.rb:21:in `success_message_present?'
     # ./code_examples/07/02/login_spec.rb:23:in `block (2 levels) in <top (required)>'

Finished in 12.08 seconds
2 examples, 1 failure

But don't worry, we'll address this limitation in the next chapter.

Second, the absence of success message does not necessarily indicate a failed login. The assertion we ended up with is more concise.

Part 4: Confirm We're In The Right Place

Before we can call our page object finished, there's one more addition we'll want to make. We'll want to add an assertion to make sure that Selenium is in the right place before proceeding. This will help add some initial resiliency to our test.

As a rule, we want to keep assertions in our tests and out of our page objects. But this is the exception.

class Login

  LOGIN_FORM  = { id: 'login' }
    USERNAME_INPUT = { id: 'username' }
    PASSWORD_INPUT = { id: 'password' }
  SUCCESS_MESSAGE = { css: '.flash.success' }
  FAILURE_MESSAGE = { css: '.flash.error' }

  def initialize(driver)
    @driver = driver
    @driver.get ENV['base_url'] + '/login'
    @driver.find_element(LOGIN_FORM).displayed?.should == true
  end
  ...

We simply add a new line to the end of our initialize method. In it we are checking to see if the login form is displayed, and making an assertion against the boolean response returned from Selenium.

The only unfortunate part of doing an assertion in the page object is that we don't have access to RSpec's matchers (e.g., be_true) out of the box. Instead we use a comparison operator to see if the boolean equals true (== true).

Now if we run our tests again, they should pass just like before. But now we can rest assured that the test will only proceed if the login form is present.

Up next, I'll cover how to make your test code more resilient.

For comprehensive examples like this (and a whole lot more), grab your copy of The Selenium Guidebook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment