Manual:Coding conventions/Selenium-Ruby
This page is obsolete. It is being retained for archival purposes. It may document extensions or features that are obsolete and/or no longer supported. Do not rely on the information here being up-to-date. See Selenium instead. |
This page describes coding conventions we follow when writing browser tests. The links section of the mediawiki-selenium README file lists the repositories that contain browser tests. There are three types of Cucumber test files in the MediaWiki codebase: Cucumber features files, Cucumber step definition files and page object files.
Željko Filipin comments
I pair with people from different teams and I work on the mediawiki-selenium gem, so I tend to see a lot of Ruby, Selenium, Cucumber and page-object code. I have noticed some code that I like and some code that I do not like.
To move the code towards what I like, I have been working on this page. I plan to implement the changes myself, if nobody does it before I have the time to do it. I do not have a timeline for that.
I would really appreciate if you would read the page (it should take you 5 minutes or so) and let me know if you agree or disagree with my thoughts, if I missed something, if something needs to be explained, if the page should have more text and less code or vice versa...
You can let me know in the page discussion section, at QA mailing list, at #wikimedia-qa connect freenode IRC channel (I am zeljkof there). Also, feel free to update the page.
General
[edit]Whitespace
[edit]Selenium tests should use the same whitespace convention (tabs, spaces...) that the repository already uses. That makes it easy for developers to work on tests. This convention is currently used in most places. ✔
tests/browser
[edit]If possible, Selenium tests should be located in tests/browser
folder. This convention is currently used in most places. ✔
Cucumber feature files
[edit]For example file see any file in features
folder of qa-browsertests repository. Feature files usually contain features and scenarios.
Scenarios
[edit]- Every scenario should be as simple as possible. It is better for a feature to have more smaller scenarios than just a few big scenarios that try to test everything. This convention is currently used in most places. ✔
- The scenarios are supposed to be as human-readable as possible. Do not turn them into a programming language - they are supposed to be a communication tool between users, product managers, testers and developers.
- Avoid mentioning implementation details in the scenarios.
- Whenever possible, specify actual test data strings in Scenarios and pass those to the tests using the regex capture aspect of Cucumber.
Example of article title defined in the page object (bad)
When I go to a nonexistent page
When(/^I go to a nonexistent page$/) do
visit(NonexistentPage)
end
class NonexistentPage < ArticlePage
def self.url
URL.url("Nonexistent_page_ijewrcmhvg34773")
end
page_url url
Fixed: article title is passed from the Scenario to the step to the page object. (good):
When I go to an uncreated page using URL Nonexistent_page_ijewrcmhvg34773
When(/^I go to an uncreated page using URL (.+)$/) do |article|
visit(NonexistentPage, :using_params => {:article_name => article})
end
class NonexistentPage < ArticlePage
include PageObject
include URL
page_url URL.url("<%=params[:article_name]%>")
end
also
Example of text typed in to page only in a step and checked only in a step (bad):
When I type a math expression
Then alt for that img should be the math expression
When(/^I type a math expression$/) do
on(EditPage).article_text=<math>3 + 2</math>
end
Then(/^alt for that img should be the math expression$/)
on(EditPage).math_image_element.element.alt.should == "3 + 2"
end
Fixed, text to be typed into page is specified in the Scenario and result to be checked also specified in the Scenario (good):
When I type <math>3 + 2</math>
Then alt for that img should be 3+2
When(/^I type (.+)$/) do |write_text|
on(EditPage).article_text=write_text
end
Then(/^alt for that img should be (.+)$/) do |alt|
on(EditPage).math_image_element.element.alt.should == alt
end
Alphabetically sorted
[edit]If a feature or scenario has more than one tag, they should be sorted alphabetically. This convention is currently used in most places. ✔
Example (good):
@clean @custom-browser @en.wikipedia.beta.wmflabs.org @firefox @login @phantomjs @test2.wikipedia.org
Required tags
[edit]Every feature in a feature file should have site- and browser- specific tags. Scenarios inherit tags from the features they belong to (see Cucumber tags documentation), so if for example the entire feature runs on a specific browser, you only need tag the feature at the top of the file, not individual scenarios.
- A site-specific tag is for example
@en.wikipedia.beta.wmflabs.org
or@test2.wikipedia.org
. The tag specifies where the feature or scenario should run. This convention is currently used in most places. ✔@clean
is a special case of a site-specific tag is . If the feature or scenario runs fine on a clean wiki, it should be tagged@clean
. This convention is currently not used. Except for a single test in the /qa/browsertests repo, at the moment we tag features or scenarios that are known to fail on a clean wiki with@needs-custom-setup
.
- A browser-specific tag is for example
@firefox
or@phantomjs
. These tags specify which browsers can run the feature or scenario. This convention is used in most projects that have CI browser tests. ✔ We also tag features or scenarios if they are known to fail with a specific browser, for example@phantomjs-bug
.
Optional tags
[edit]Some features or scenarios can have an optional tag.
- If the feature or scenario requires the user to log in, it should have
@login
tag. This convention is currently used in most places. ✔ - If the feature or scenario requires custom browser configuration, it should be tagged
@custom-browser
. This convention is currently used in most places. ✔ - We need to tag tests that are quick or slow to run. My suggestion is
@quick
and@slow
tags.@quick
tag could be used to create a Jenkins job that would run after every patch set submission to Gerrit, or every time a commit is merged into master branch.@slow
tag could be used to create a Jenkins job that would run once a day. This convention is currently not used.
Extension tags
[edit]If your feature requires the presence of optional MediaWiki extensions, be sure to include an @extension-
tag for each. These tags will help keep your test suite more deterministic by allowing Cucumber to skip features that would otherwise fail falsely due to the wiki's configuration.
@extension-visualeditor
Feature: VisualEditor Mobile
Scenario: VisualEditor Provides Bold
Given I am logged into the mobile website
When I look at the VisualEditor toolbar
Then I see a bold button
Before attempting to execute your feature, Cucumber will check that the wiki has these extensions installed and enabled. If any of the dependencies aren't met, the runner will skip the feature and warn the user.
Cucumber step definition files
[edit]For example file see any file in step_definitions
folder of qa-browsertests repository. Step definition files usually contain Given
, When
and Then
steps.
Page objectives pattern
[edit]Direct calls to Selenium function are not supposed to be used. Use PageObject. This convention is currently used in most places. ✔
Example (good):
Given(/^I am at Log in page$/) do
visit LoginPage
end
Example (bad):
Given(/^I am at Log in page$/) do
@browser.goto "#{ENV['MEDIAWIKI_URL']}Special:UserLogin"
end
Simplicity
[edit]In general, code in step definition files should be as simple as possible. Ideally, just one or two lines per step. All complicated code should be moved to page objects. This convention is currently used in most places. ✔
Example (good):
Given(/^I am at Log in page$/) do
visit LoginPage
end
When(/^I log in with incorrect password$/) do
on(LoginPage).login_with(ENV["MEDIAWIKI_USER"], "incorrect password")
end
Grouped by type
[edit]Steps should be grouped by type. Given
and When
steps should be grouped in one group, Then
steps should be grouped separately.
Example (good):
Given(/^I am at Log in page$/) do
visit LoginPage
end
When(/^I log in with incorrect password$/) do
on(LoginPage).login_with(ENV["MEDIAWIKI_USER"], "incorrect password")
end
Then(/^feedback should be (.+)$/) do |feedback|
on(LoginPage) do |page|
page.feedback_element.when_present.click
page.feedback.should match Regexp.escape(feedback)
end
end
Then(/^Log in element should be there$/) do
on(LoginPage).login_element.should exist
end
This convention is currently not used. At the moment we do not combine Given
and When
steps in one group.
Alphabetically sorted
[edit]Inside a group, steps should be sorted alphabetically by step name. That moves steps with similar name close to each other. Steps with similar name usually have similar functionality, making them good candidates for merging. This convention is currently used in most places. ✔
Example (good):
Then(/^Log in element should be there$/) do
on(LoginPage).login_element.should exist
end
Then(/^Log in page should open$/) do
@browser.url.should match Regexp.escape("Special:UserLogin")
end
Assertions
[edit]We are using rspec-expectations assertions in step definition files. Assertions should be used only in Then
steps. Using assertion in Given
or When
step is usually a sign that the scenario is too big and should be split into two or more smaller scenarios. This convention is currently used in most places. ✔
expect
or should
[edit]We should use rspec-expectations expect
syntax.
Example (good, expect
syntax):
Then(/^Log in element should be there$/) do
expect(on(LoginPage).login_element).to exist
end
This convention is currently not used. At the moment we are using old should
syntax.
Example (bad, should
syntax):
Then(/^Log in element should be there$/) do
on(LoginPage).login_element.should exist
end
Page object files
[edit]For example file see any file in features/support/pages
folder of qa-browsertests repository. Step definition files usually contain page URL, page elements and methods.
URL
[edit]Page URL is optional. It is used in step definitions when a Given
or When
step needs to go directly to a page, or when a Then
step needs to check page URL. This convention is currently used in most places. ✔
Example (good):
class LoginPage
include URL
page_url URL.url("Special:UserLogin")
end
Page elements
[edit]Simple
[edit]Simple page elements should be defined using page-object Ruby gem API. This convention is currently used in most places. ✔
Example (good):
a(:edit, text: "Edit source")
page-object gem shortcuts
[edit]Code is more readable if shortcuts are not used.
Example (good):
page.edit_element.click
Example (badbad):
page.edit
Complicated
[edit]Elements that are complicated to find should pass blocks finding the elements to the page-object API. This convention is currently used in most places. ✔
Example (good):
unordered_list(:search_results, class: "mw-search-results")
li(:second_result_wrapper) do |page|
page.search_results_element.list_item_element(index: 1)
end
link(:second_result) do |page|
page.second_result_wrapper_element.div_element(class: "mw-search-result-heading").link_element
end
Methods
[edit]If a page has complicated functionality, a method in its page class is usually the best place for it. This convention is currently used in most places. ✔
Example (good):
class LoginPage
include PageObject
def login_with(username, password)
self.username_element.when_present.send_keys(username)
self.password_element.when_present.send_keys(password)
login_element.fire_event("onfocus")
login_element.when_present.click
end
end
If the method returns page element, it's name should end in _element
. This convention is currently used in most places. ✔
Example (good):
class LoginPage
include PageObject
def statement_name_element(group_index)
@browser.element(css: ".wb-claimlistview:nth-child(#{group_index}) div.wb-claim-name")
end
end
Example (bad):
class LoginPage
include PageObject
def statement_name(group_index)
@browser.element(css: ".wb-claimlistview:nth-child(#{group_index}) div.wb-claim-name")
end
end
Hooks
[edit]Complex, exceptional behavior needing to be used by multiple tests may be specified in the file under support/hooks.rb
Some examples from the VisualEditor repository:
A hook to keep the browser open if an env var is set:
at_exit do
$browser.close unless ENV["KEEP_BROWSER_OPEN"] == "true"
end
A "Before" hook that will set up an edited page for manipulation by tests:
Before("@edit_user_page") do
if (!$edit_user_page or !(ENV["REUSE_BROWSER"] == "true")) and @browser
step "I am logged in"
step "I am at my user page"
step "I edit the page with Editing with"
$edit_user_page=true
end
end
A "Before" hook that sets up a page and selects a particular string on the page to be manipulated by tests:
Before("@make_selectable_line") do
if (!$make_selectable_line or !(ENV["REUSE_BROWSER"] == "true")) and @browser
step "I am logged in"
step "I am at my user page"
step "I click Edit for VisualEditor"
step "I type in an input string"
step "select the string"
$make_selectable_line=true
end
end