Skip to content

Instantly share code, notes, and snippets.

@Juraci
Last active August 29, 2015 14:05
Show Gist options
  • Save Juraci/67206782acbbbf2e6dc9 to your computer and use it in GitHub Desktop.
Save Juraci/67206782acbbbf2e6dc9 to your computer and use it in GitHub Desktop.
Pattern to describe responsibilities for functional test automation architecture
# Examples bellow assume the following stack:
# Ruby 1.9.3 or superior
# Capybara most recent version
# RSpec 2.14
# Selenium-webdriver most recent version
# The scenario:
# * Automate the server creation with Standard Flavor
# * Automate the server creation with Performance 1 Flavor
# 1- First attempt: using directly calls to the Capybara::DSL (no page objects)
describe 'Server Creation' do
include Capybara::DSL
# page is a shortcut to Capybara.current_session
before :each do
visit 'https://appurl.com'
page.within '.page_header' do
fail 'Not on Cloud Servers Page' unless page.has_text 'Cloud Servers'
end
page.click_button 'Create Server'
page.within '.page_header' do
fail 'Not on Create Server page' unless page.has_text? 'Create Server'
end
end
context 'When it uses the Standard Flavor' do
it 'can create the server' do
page.within '#server-identity-section' do
page.fill_in 'server_name', with: 'MyStandardServer'
page.select 'Dallas (DFW)', from: 'virtual-server-provider-select'
end
page.within '#image-list' do
page.find("li[class*='linux']").click
page.find("li[class*='ubuntu']").click
page.find(:xpath, "label[textt()='14.04 LTS (Trusty Tahr)']").click
end
page.within '.flavor-section' do
page.find(:xpath, 'div[text()="Standard"]').click
end
page.click_button 'Create Server'
page.within '#server_name' do
page.should have_text 'MyStandardServer'
end
page.within '#server_flavor_name' do
page.should have_text '512MB Standard Instance'
end
end
end
context 'When it uses the Performance 1 Flavor' do
it 'can create the server' do
page.within '#server-identity-section' do
page.fill_in 'server_name', with: 'MyPerformanceServer'
page.select 'Dallas (DFW)', from: 'virtual-server-provider-select'
end
page.within '#image-list' do
page.find("li[class*='linux']").click
page.find("li[class*='ubuntu']").click
page.find(:xpath, "label[textt()='14.04 LTS (Trusty Tahr)']").click
end
page.within '.flavor-section' do
page.find(:xpath, 'div[text()="Performance 1"]').click
end
page.click_button 'Create Server'
page.within '#server_name' do
page.should have_text 'MyPerformanceServer'
end
page.within '#server_flavor_name' do
page.should have_text '1 GB Performance Instance'
end
end
end
end
# Benefits with this approach
# * Capybara > pure selenium
# Problems with this approach
# * the knowledge to interact with the web elements is spread and duplicated across the tests
# * css and xpath selectors exposed in the test body making it hard to read and understand (readability)
# * if more tests are added a simple change in the app code can potentially generate several hours/days of test maintenance (maintainability)
# Examples bellow assume the following stack:
# Ruby 1.9.3 or superior
# Capybara most recent version
# (new) CapybaraPageObjects most recent version
# RSpec 2.14
# Selenium-webdriver most recent version
# The scenario:
# * Automate the server creation with Standard Flavor
# * Automate the server creation with Performance 1 Flavor
# 2- Second attempt: Refactoring the previous attempt using page objects
# simply as the interface between the tests and the web app
describe 'Server Creation' do
before :each do
@list_view = Servers::Pages::ListView.visit
fail 'Not on Cloud Servers Page' unless @list_view.current_page?
@list_view.click_create_server
@create_view = Servers::Pages::CreateView.new
fail 'Not on Create Server Page' unless @create_view.current_page?
end
context 'When it uses the Standard Flavor' do
it 'can create the server' do
@create_view.identity_section.type_name 'MyStandardServer'
@create_view.identity_section.select_region 'Dallas (DFW)'
@create_view.image_list.select_image('Linux', 'Ubuntu', '14.04 LTS (Trusty Tahr)')
@create_view.flavor_section.select_flavor 'Standard'
@create_view.click_create_server
@details_view = Servers::Pages::DetailsView.new
@details_view.header.server_name.should == 'MyStandardServer'
@details_view.details_section.flavor.should == '512MB Standard Instance'
end
end
context 'When it uses the Performance 1 Flavor' do
it 'can create the server' do
@create_view.identity_section.type_name 'MyPerformanceServer'
@create_view.identity_section.select_region 'Dallas (DFW)'
@create_view.image_list.select_image('Linux', 'Ubuntu', '14.04 LTS (Trusty Tahr)')
@create_view.flavor_section.select_flavor 'Performance 1'
@create_view.click_create_server
@details_view = Servers::Pages::DetailsView.new
@details_view.header.server_name.should == 'MyStandardServer'
@details_view.details_section.flavor.should == '1 GB Performance Instance'
end
end
end
# Benefits with this approach
# * Well defined single responsibility, the knowledge to interact with the web elements is isolated
# inside the Pages and Components (improved design)
# * Easier to read the examples (improved readability)
# Problems with this approach
# * There are still procedural actions repeated across similar tests like:
@create_view.identity_section.type_name 'MyStandardServer'
@create_view.identity_section.select_region 'Dallas (DFW)'
@create_view.image_list.select_image('Linux', 'Ubuntu', '14.04 LTS (Trusty Tahr)')
@create_view.flavor_section.select_flavor 'Standard'
@create_view.click_create_server
# The only change is the data used to select the flavor
# The solution would be to abstract the procedural events in a layer that represents the services
# Examples bellow assume the following stack:
# Ruby 1.9.3 or superior
# Capybara most recent version
# CapybaraPageObjects most recent version
# RSpec 2.14
# Selenium-webdriver most recent version
# The scenario:
# * Automate the server creation with Standard Flavor
# * Automate the server creation with Performance 1 Flavor
# 3- Third attempt: Refactoring the previous attempt using "Steps" objects
# as a place to abstract procedural actions
describe 'Server Creation' do
let(:steps) { Services::Servers.new }
before :each do
Steps::Servers.new.navigate_to_create_server_page
end
context 'When it uses the Standard Flavor' do
before do
@server = DataSetup::Servers::Server.new
.name = 'MyStandardServer'
.region = 'Dallas (DFW)'
.image_so = 'Linux'
.image_distro = 'Ubuntu'
.image_version = '14.04 LTS (Trusty Tahr)'
.flavor = "Standard"
end
it 'can create the server' do
steps.create_server @server
@details_view = Servers::Pages::DetailsView.new
@details_view.header.server_name.should == @server.name
@details_view.details_section.flavor.should == @server.flavor
end
end
context 'When it uses the Performance 1 Flavor' do
before do
@server = DataSetup::Servers::Server.new
.name = 'MyPerformanceServer'
.region = 'Dallas (DFW)'
.image_so = 'Linux'
.image_distro = 'Ubuntu'
.image_version = '14.04 LTS (Trusty Tahr)'
.flavor = "Performance 1"
end
it 'can create the server' do
steps.create_server @server
@details_view = Servers::Pages::DetailsView.new
@details_view.header.server_name.should == @server.name
@details_view.details_section.flavor.should == @server.flavor
end
end
end
# Benefits with this approach
# * Well defined responsibilities:
# - * Page Objects: Only interact with web elements (no procedural actions)
# - * Steps Objects: Responsible to abstract common procedural actions
# - * RSpec examples: Responsible to arrange and apply expectations upon a given chain of events
# Problems with this approach
# * The increased number of layers/abstractions makes the learning curve harder
# * The more procedural code inside a single method the less reusable it is.
# This can lead to a combinatorial explosion inside the Service Layer objects
Responsibilities and best practices
The Page Objects
The original Page Objects documentation (https://code.google.com/p/selenium/wiki/PageObjects)
says it has basically two responsibilities, which are:
- Represent the services offered by a page or part of a page and,
- it knows how to interact with the web elements
Simply facing this definition with coding best practices we can spot
that the Single Responsibility Principle is broken by that definition.
A Page Object following that definition will have at least two reasons to change, which are:
- If the service changes the page object will have to change the representation of that service
- If the web elements changes the page object will also have to change the way it interacts
with the elements
Each and every project that I've worked on that tried to follow this
original definition (mixing responsibilities) ended up with a code base that
was very hard to maintain, had lots of code smells and as a result the tests were
getting unstable and untrustworthy.
I decided to design the Page Objects as having one single responsibility which is:
- It is the only part that knows how to interact with the web elements, abstracting
the selectors from the other parts of the test architecture.
- No procedural actions
- No navigation flows
- No domain knowledge
That leads us to small and easy to maintain page objects but also raises the need for
a layer in which we can add Domain logic and abstract procedural actions and flows.
The Steps
The steps idea is very simple:
It abstracts common procedural actions that are shared between tests. Close to the Domain intentions.
For instance a very common step would be:
module Steps
class Login
def login_with(user, password)
login = Login.visit
login.type_user_name user
login.type_password password
login.click_login
end
end
end
It still suffers from code smells like 'procedural methods', 'long methods'
but at least this is isolated in its own class, and not mixed up with page objects.
Why is it different from Cucumber steps?
Although you can use the Steps to abstract common actions and Domain facing services
you should not be obligated to use only steps in your test runner (RSpec for instance).
You are free to combine Steps with directly page objects calls (respecting the responsibilities of course).
Which gives a excellent flexibility to your RSpec examples so you can abstract only the blocks
that makes sense to you and expose more important interactions in your test body, like the ones that return
values to be asserted.
The test layer
The test layer in the case of RSpec, Junit, Nunit... will be responsible to arrange Steps and create
expectations/assertions upon results that are returned from the page objects.
- The only place where expectations/assertions should be.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment