This post is the fourth in a series of “Getting Started with Selenium Testing” posts from Dave Haeffner, a noted expert on Selenium and automated testing, and a frequent contributor to the Sauce blog and Selenium community. This series is for those who are brand new to test automation with Selenium and a new chapter will be posted every Tuesday (eight chapters in all).
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.An Example
Part 1: Create A Page Object
# filename: login.rbclass 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 endWe 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 wheresuccess_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.rbrequire '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 endAt 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.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? endLastly, 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 endNow 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 endThere 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 ' Finished in 12.08 seconds 2 examples, 1 failureBut 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.