Last active
August 29, 2015 14:05
-
-
Save Juraci/67206782acbbbf2e6dc9 to your computer and use it in GitHub Desktop.
Pattern to describe responsibilities for functional test automation architecture
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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