You are on page 1of 37

Pageness at LinkedIn

Wade Catron

1
Agenda
• Background
• Core Technology Choices
• Barebones Test Framework
• Introducing Pageness
• Cool Tricks
• Supporting Infrastructure
• Wrapup & Questions

Background 2
Background

Background 3
LinkedIn Web Apps
• Java based web applications
• Complex social interactions
• Users of all shapes and sizes
• Variations of common flows
• Hundreds of pages and states
• Plenty of rich, gooey ajaxiness

Background 4
LinkedIn SDLC
• Very frequent product enhancements
• New features developed in parallel
• 10+ feature branches per release
• Rapid dev/test/release cycle
• Tons of testing environments, some with
disparate data sets

Background 5
LinkedIn QA
• QA staff embedded in dev teams
• Expert domain knowledge
• New feature/release validation
• Regression testing of existing product
• Owns thousands of test specs
• Strong scripting skillz

Background 6
QA Automation Goals
• Automate everything, early
• Produce long lived test assets
• Share code between teams
• Strive for minimal maintenance costs
• Author tests with the quickness
• Balance “easy” vs “flexible/powerful”

Background 7
Core Technology

Core Technology Choices 8


Core Technology Choices
• Ruby
– Lightweight, low learning curve
– Existing skillset within our organization
– Minimal boilerplate code required
– Flexible

• Test::Unit
– Simple, known, xUnit pattern
– Extensible
– One script per test

Core Technology Choices 9


Barebones Framework

Barebones Framework 10
Test::Unit Example
Class SignInTest < Test::Unit::TestCase

def setup
@env = Environment.new
end

def test_sign_in
@env.selenium.open(@env.base_url)
@env.selenium.type(“id=session-email”,
@env.main_user.email)
...
...
end

end

Barebones Framework 11
Ruby Selenium Client Driver
• Simple, verb oriented syntax
• Support for ajax operations
• Well maintained

Barebones Framework 12
Selenium Client Example

What exactly is happening here?

sel.click_and_wait(”//id('main')/div/div[2]/dl[1]/dt[5]/a")

sel.click("id=CONNECTIONS-statusSettingsParam-statusSettings”)
sel.click_and_wait(“//input[@class='btn-primary']”)

assert(sel.is_element_present("//div[@class='alert succ']"))

Barebones Framework 13
Selenium Client Example

Well-placed comments improve readability a bit


# Click the Privacy Settings link
sel.click_and_wait(”//id('main')/div/div[2]/dl[1]/dt[5]/a")

# Enable status sharing with connections only and click Save


sel.click("id=CONNECTIONS-statusSettingsParam-statusSettings”)
sel.click_and_wait(“//input[@class='btn-primary']”)

# Make sure we get a happy green message


assert(sel.is_element_present("//div[@class='alert succ']"))

Barebones Framework 14
Fundamental Problems
• Hardcoded locators
– brittle tests
– bad for readability
– failure/error analysis is tedious!
 
• Driver dependent code

Barebones Framework 15
Pageness

Introducing Pageness 16
We Need Driver Abstraction!

Introducing Pageness 17
Desired Outcome
• Shared locators
• Cleaner, more readable scripts
• More idiomatic, noun based API

Taza-like Grammatical Pattern


settings_page = SettingsPage.new(@env)
settings_page.privacy_settings_link.click_and_wait

privacy_page = PrivacySettingsPage.new(@env)
privacy_page.connections_only_radio_option.click
privacy_page.save_button.click_and_wait

settings_page.success_message.assert_present

Introducing Pageness 18
Describing A Page

class SignInPage < Page

    element :email_field, "session_key-login"


    element :password_field, "session_password-login"
    element :sign_in_button, "session_login"

end

Introducing Pageness 19
The Magical Page Class
sign_in_page = SignInPage.new(@env)

Page Object with Dynamically Generated Element Methods


def email_field
return Element.new(:email_field, ‘session-key_login’)
end

def password_field
return Element.new(:password_field, ‘session-passwd_login’)
end

def sign_in_button
return Element.new(:sign_in_button, ‘session_login’)
end

Introducing Pageness 20
What’s an Element?
sign_in_page.email_field

Element Object
@name = :email_field
@locator = ‘session-key_login’

def method_missing(meth, *args)


if args.empty?
@sel.send(meth, locator)
else
@sel.send(meth, locator, args)
end
End

Introducing Pageness 21
Element Specific Interface
sign_in_page.email_field.type(“email@linkedin.com”)

Element Object
@name = :email_field
@locator = ‘session-key_login’

def method_missing(meth, *args)


if args.empty?
@sel.send(meth, @locator)
else
@sel.send(meth, @locator, args)
end
end

Introducing Pageness 22
Element Specific Interface
sign_in_page.email_field.type(“email@linkedin.com”)

Element Object
@name = :email_field
@locator = ‘session-key_login’

def method_missing(meth, *args)


if args.empty?
@sel.send(meth, @locator)
else
@sel.send(meth, @locator, args)
end
end

Introducing Pageness 23
Element Specific Interface
sign_in_page.email_field.type(“email@linkedin.com”)

Element Object
@name = :email_field
@locator = ‘session-key_login’

def method_missing(meth, *args)


if args.empty?
@sel.send(meth, @locator)
else
@sel.send(meth, @locator, args)
end
end

Introducing Pageness 24
Element Specific Interface
sign_in_page.email_field.type(“email@linkedin.com”)

Element Object
@name = :email_field
@locator = ‘session-key_login’

def method_missing(meth, *args)


if args.empty?
@sel.send(meth, @locator)
else
@sel.send(meth, @locator, args)
end
end


@sel.type(‘session-key_login’, ‘email@linkedin.com’)

Introducing Pageness 25
End Result
• Shared locators
• Cleaner, more readable test scripts
• More idiomatic, noun based API

settings_page = SettingsPage.new(@env)
settings_page.privacy_settings_link.click_and_wait

privacy_page = PrivacySettingsPage.new(@env)
privacy_page.connections_only_radio_option.click
privacy_page.save_button.click_and_wait

settings_page.success_message.assert_present

Introducing Pageness 26
Another Example
# Sign in to the app
sign_in_page = SignInPage.new(@env)
sign_in_page.email_field.type(email)
sign_in_page.password_field.type(email)
sign_in_page.login_button.click_and_wait

# Make sure the Welcome message shows up


home_page = HomePage.new(@env)
home_page.welcome_message.assert_present

# Make sure the Mini panel things become visible


home_page.mini_panel_div.wait_for_visible
home_page.mini_panel_div.assert_visible
home_page.mini_panel_profile_link.assert_visible
home_page.mini_panel_headline.assert_visible
home_page.mini_panel_location.assert_visible
Background 27
Cool Tricks

Background 28
Build Custom Methods
class Element

def method_missing(meth, *args)


def assert_present
assert(self.is_element_present)
end

def click_if_present
self.click if self.is_element_present
end

def type_each_key(string)
string.each_byte do |byte|
char = byte.chr
@sel.key_down(locator, char)
@sel.key_press(locator, char)
@sel.key_up(locator, char)
end
end
Cool Tricks 29
Loop over all Elements
HomePage class definition
class HomePage < Page

element :welcome_message, "session_key-login"


  element :primary_navigation_menu, "session_password-login"
  element :ad_slot_1, "session_key-login”
element :network_updates_div, "session_key-login”

end

Test script
home_page = HomePage.new(@env)

home_page.elements.each do |element|
  element.assert_present
end

Cool Tricks 30
Contexual Locators
Example: Iterating through a table or list to collect results

<ul id="messages">
<li>
<h1>Invitation to reconnect</h1>
<p>It's been too long, man!..</p>
</li>
<li>
<h1>Chunky Bacon</h1>
<p>so good</p>
</li>
...
</ul>

index = 1
locator = “//ul[@id='messages']/li[#{index}]/h1”
while @sel.is_element_present(locator)
results << @sel.get_text(locator)
index += 1
locator = “//ul[@id='messages']/li[#{index}]/h1”
end

Cool Tricks 31
A Pagier Solution
element :subject_by_index,
Proc.new { |index| "//ul[@id=‘messages’]/li[#{index}]” }

inbox_page = InboxPage.new(@env)

messages = []

count = 1
while inbox_page.subject_by_index.is_element_present(:index => count) do
messages << inbox_page.subject_by_index.get_text(:index => count)
count += 1
end

assert_equal(messages.length, 10)
assert_true(messages.include? “Welcome to LinkedIn”)

Cool Tricks 32
Supporting Infrastructure

Wrapup 33
Integrated Bits
• Test helper and utility classes
• Multi-threaded test runner for parallel
execution
• Environmental data classes for urls/credentials
• Custom test class properties: priority,
grouping, requires_isolation
• HTTP based data generator integration

Supporting Infrastructure 34
A Few Statistics
• Adopted page model 6 months ago
• Wrote 887 test scripts between 5 teams
• Daily average of 28 test-related commits
• Average number of elements per page: 18
• 250+ defined pages

Wrapup & Questions 35


Thanks!

Shama Butala-Katkar
Boris Roussev
Helen Shyu
Priyanka Salvi

Background 36
?
wcatron@linkedin.com

Questions 37

You might also like