QA/Execution/Web Testing/Docs/Automation/StyleGuide: Difference between revisions

From MozillaWiki
Jump to navigation Jump to search
No edit summary
(Undo revision 1105014 by Jlorenzo (talk) - I edited the wrong page)
 
(22 intermediate revisions by 3 users not shown)
Line 1: Line 1:
The goal of the style guide is to try provide rules to write code that looks the same no matter what project. It is a guide and is always up for discussion by the team. We have created [https://github.com/AutomatedTester/mozwebqa-test-templates templates] based on the details below.
The goal of the style guide is to try provide rules to write code that looks the same no matter what project. It is a guide and is always up for discussion by the team.


= All Files =
= All Files =


== File Headers ==
== File Headers ==
* At the top of each file should have python file header
* Each file should have a completed copy of the [http://www.mozilla.org/MPL/2.0/ MPL2] license block, immediately followed by an empty line.
<pre class="brush:py;toolbar:false;">
    #!/usr/bin/env python
</pre>
* Each file should have a completed copy of the [http://www.mozilla.org/MPL/boilerplate-1.1/mpl-tri-license-sh MPL]
* Each file should pass [http://www.python.org/dev/peps/pep-0008/ PEP8] except for line length, see below.   
* Each file should pass [http://www.python.org/dev/peps/pep-0008/ PEP8] except for line length, see below.   
** I.e. parameters  should have a comma and a space e.g.
<pre class="brush:py;toolbar:false;">
    # Good
    def method(self, parameter)


    # Bad
<source lang="python">
    def method(self,parameter)
# Good
</pre>
def method(self, parameter)
** Lines should try not to have more than 100 characters.
 
** Indenting should be a soft tab (4 spaces) as common with in Python. Do not mix tabs and spaces!
# Bad
** There should be no whitespace at the end of the file (as per PEP8)
def method(self,parameter)
** Comments should be on the line above. Remember to update comments when changing code so that code matches the comments.
</source>
** Class names should be in Pascal style as this is Python idiomatic.
 
<pre class="brush:py;toolbar:false;">
* Lines should try not to have more than 100 characters.
    # Good
* Docstrings should conform to [http://www.python.org/dev/peps/pep-0257/ PEP0257] and should be on a single line wherever possible.
    class TestThisSite:
 
<source lang="python">
# Good
def click_login():
"""Clicks the login link."""
 
# Bad
def click_login():
"""
Clicks the login link.
"""
</source>
 
Where not possible, the first line should be a summary.
 
<source lang="python">
# Good
def login():
"""Logs in.
 
Clicks the login link and then waits for the home page to load.
 
"""
 
# Bad
def login():
"""Logs in.
Clicks the login link and then waits for the home page to load."""
</source>
 
* Indenting should be a soft tab (4 spaces) as common with in Python. Do not mix tabs and spaces!
* There should be no whitespace at the end of the file (as per PEP8).
* Comments should be on the line above. Remember to update comments when changing code so that code matches the comments.
* Class names should be in Pascal style as this is Python idiomatic.
 
<source lang="python">
# Good
class TestThisSite:
      
      
    # Bad
# Bad
    class test_this_site:
class test_this_site:
</pre>
</source>


= Page Objects =
= Page Objects =
== General ==
== General ==
* All Page Objects should inherit from Page in page.py.
* All page objects should inherit from <code>Page</code> in page.py.
* Page Objects should not do asserts. This should be done within the test.
* Page objects should not do asserts. This should be done within the test.
* Each Page should be grouped within one module.
* Each page should be grouped within one module.
* If using mutliple words to describe a module separate them with underscores '_'
* If using multiple words to describe a module separate them with underscores '_'
    test_search.py
* Timeout time should be taken from pytest-mozwebqa via <code>page.py's</code> timeout property.
* Timeout time should be taken from pytest-mozwebqa via page.py's timeout property.
* Single quotes (') should be used instead of double (") throughout.
* Single quotes (') should be used instead of double (") throughout.
* Methods should have a single purpose.
== Logic ==
* Methods should not contain logic that depends on properties of the page. The logic and expectations should be within the test, and adding this to the page object could guard your tests against genuine failures.
<source lang="python">
# Good
def click_login(self)
    self.selenium.find_element(*self._login_locator).click()
# Bad
def click_login(self)
    if not self.is_user_logged_in:
        self.selenium.find_element(*self._login_locator).click()
    else:
        pass
</source>


== Locators ==
== Locators ==
** Locator variables should be prefixed with _ to show that it is "[http://docs.python.org/tutorial/classes.html#private-variables private]".
* Locator variables should be prefixed with <code>_</code> to show that it is [http://docs.python.org/tutorial/classes.html#private-variables private].
** Variables should be descriptive of the area and not clash with any properties.
* Variables should be descriptive of the area and not clash with any properties.
** Suffix of _locator.
* Should have a suffix of <code>_locator</code>.
** Accessing Locators should be done through a property method as this keeps the locator as readonly.
* Accessing locators should be done through a property or method as this keeps the locator as read-only.
<pre class="brush:py;toolbar:false;">
 
    @property
<source lang="python">
    def search_box(self):
@property
        return self.search_box_locator
def search_term(self):
</pre>
    return self.selenium.find_element(*self._search_box_locator).value
* This approach can also be used with get_* calls with Selenium making it more idiomatic for the call.
</source>
<pre class="brush:py;toolbar:false;">
 
    @property
    def page_title(self):
        return self.selenium.get_title()
</pre>
* We should use locators in the following order of preference (there will be exceptions):
* We should use locators in the following order of preference (there will be exceptions):
** ID
** ID
** Name
** Name
** CSS
** Class name
** CSS selector
** XPath
** XPath
* Locators should include the strategy instead of using implied strategies.
<pre class="brush:py;toolbar:false;">
    # Good
    _my_locator = "id=content"
   
    # Bad
    _my_locator = "content"
</pre>
* CSS locators should use whitespace for readability when using direct descendants.
* CSS locators should use whitespace for readability when using direct descendants.
<pre class="brush:py;toolbar:false;">
 
    # Good
<source lang="python">
    _my_locator = "css=#content > p > a"
# Good
_my_locator = "css=#content > p > a"
      
      
    # Bad
# Bad
    _my_locator = "css=#content>p>a"
_my_locator = "css=#content>p>a"
</pre>
</source>
 
* Use Python tuples to define locators:


* With Webdriver, use a Python tuple:
<source lang="python">
<pre class="brush:py;toolbar:false;">
# Good
    # Good
_my_locator = (By.ID, "content")
    _my_locator = (By.ID, "content")
</source>
</pre>


== Actions ==
== Actions ==
* Methods that perform actions on the page should indicate the action in the method name.
* Methods that perform actions on the page should indicate the action in the method name.
<pre class="brush:py;toolbar:false;">
    # Good
    def click_report_with_length(length)


    # Bad
<source lang="python">
    def report_length(length)
# Good
</pre>
def click_report_with_length(length)


# Bad
def report_length(length)
</source>
* Actions should wait for the appropriate action to complete. This could be an implicit or explicit wait. For example, clicking a login button might explicitly wait for a username field to be visible.


== Advanced: Page Regions ==
== Advanced: Page Regions ==
In some circumstances, for example where a header/navigation is common across the website, we will use a page region. The page region is a child class of the base Page Object, which is inherited by all page objects. This means that the navigation can be reached from any page object and herein lies the DRY!
In some circumstances, for example where a header/navigation is common across the website, we will use a page region. The page region is a child class of the base Page object, which is inherited by all page objects. This means that the navigation can be reached from any page object and herein lies the DRY!


A brief example:
A brief example:
<pre class="brush:py;toolbar:false;">
 
<source lang="python">
class BasePage(Page):
class BasePage(Page):


Line 113: Line 153:
         @def click_login(self):
         @def click_login(self):
             self.selenium.find_element(*self._login_link).click()
             self.selenium.find_element(*self._login_link).click()
</pre>
</source>


Referring to this page region with a property makes it very readable and concise from within the test. Clicking home during a test would be performed like this:
Referring to this page region with a property makes it very readable and concise from within the test. Clicking login during a test would be performed like this:
<pre class="brush:py;toolbar:false;">
<source lang="python">
    page_obj.header.click_login()
my_page.header.click_login()
</pre>
</source>


Another example where this might be used is on a search results page, the page region being the search results element.
Another example where this might be used is on a search results page, the page region being the search results element.
Line 124: Line 164:
= Tests =
= Tests =
* Module names should be called test_ and then behavioral areas.
* Module names should be called test_ and then behavioral areas.
     test_search_positive.py
     test_search.py
* Test method signature should include mozwebqa to use pytest-mozwebqa plugin.
* Test method signature should include mozwebqa to use pytest-mozwebqa plugin.
<pre class="brush:py;toolbar:false;">
 
    def test_example(self, mozwebqa):
<source lang="python">
</pre>
def test_example(self, mozwebqa):
</source>
 
* Test method names should always show the intent of the test case.
* Test method names should always show the intent of the test case.
<pre class="brush:py;toolbar:false;">
    # Good
    def test_that_advanced_search_does_not_find_item(self, mozwebqa):


    # Bad
<source lang="python">
    def test_advanced_search(self, mozwebqa):
# Good
</pre>
def test_that_advanced_search_does_not_find_item(self, mozwebqa):
 
# Bad
def test_advanced_search(self, mozwebqa):
</source>
 
== Assertions ==
 
* Tests should handle the asserts -- not the page objects.
* Tests should handle the asserts -- not the page objects.
* Tests should use Python's native [https://docs.python.org/2/reference/simple_stmts.html#the-assert-statement assert] statement.
** Note that this is a change from our previous standard of using the [https://wiki.mozilla.org/Web_Testing/Automation/UnittestZero UnittestZero] package.
* When doing equivalency assertions, put the expected value first, followed by the actual value, for example:
<source lang="python">
assert 'expected' == 'actual'  # good
assert 'actual' == 'expected'  # bad
</source>
* When doing negative equivalency, use != and put the unexpected value first, followed by the actual value, for example:
<source lang="python">
assert 'unexpected' != 'actual'  # good
assert 'actual' != 'unexpected'  # bad
</source>
* To directly cause a test to fail raise an AssertionError with an appropriate message, for example:
<source lang="python">
raise AssertionError('message')
</source>
* See [http://pytest.org/latest/assert.html pytest's documentation on asserts] for more help.


= Size of patches =
= Size of patches =
To make sure that we can review your patch as quickly and efficiently as possibly we would like patches to have 1 test in them and the necessary changes to the Page Objects. This also limits the chances of merge conflicts later.
To make sure that we can review your patch as quickly and efficiently as possibly we would like patches to have a single test in them and the necessary changes to the page objects. This also limits the chances of merge conflicts later.


== Using new and old standards together ==
== Using new and old standards together ==
As we (and Selenium and automation) develop more knowledge some projects might fall behind the standards in this style guide. It can be tempting to want to fix all of the outdated style but in order to keep patches/pulls small (see above!) we are happy to have new and old standards of code sit side by side. As we regularly review and update tests the project will be brought completely up to our current standards.
As we (and Selenium and automation) develop more knowledge some projects might fall behind the standards in this style guide. It can be tempting to want to fix all of the outdated style but in order to keep patches/pulls small (see above!) we are happy to have new and old standards of code sit side by side. As we regularly review and update tests the project will be brought completely up to our current standards.


Or if you prefer, log a Pivotal job or an issue to have a section of code addressed separately to the job you are doing.
Or if you prefer, log a GitHub issue to have a section of code addressed separately to the job you are doing.

Latest revision as of 16:13, 10 November 2015

The goal of the style guide is to try provide rules to write code that looks the same no matter what project. It is a guide and is always up for discussion by the team.

All Files

File Headers

  • Each file should have a completed copy of the MPL2 license block, immediately followed by an empty line.
  • Each file should pass PEP8 except for line length, see below.
# Good
def method(self, parameter) 

# Bad
def method(self,parameter)
  • Lines should try not to have more than 100 characters.
  • Docstrings should conform to PEP0257 and should be on a single line wherever possible.
# Good
def click_login():
"""Clicks the login link."""

# Bad
def click_login():
"""
Clicks the login link.
"""

Where not possible, the first line should be a summary.

# Good
def login():
"""Logs in.

Clicks the login link and then waits for the home page to load.

"""

# Bad
def login():
"""Logs in.
Clicks the login link and then waits for the home page to load."""
  • Indenting should be a soft tab (4 spaces) as common with in Python. Do not mix tabs and spaces!
  • There should be no whitespace at the end of the file (as per PEP8).
  • Comments should be on the line above. Remember to update comments when changing code so that code matches the comments.
  • Class names should be in Pascal style as this is Python idiomatic.
# Good
class TestThisSite:
    
# Bad
class test_this_site:

Page Objects

General

  • All page objects should inherit from Page in page.py.
  • Page objects should not do asserts. This should be done within the test.
  • Each page should be grouped within one module.
  • If using multiple words to describe a module separate them with underscores '_'
  • Timeout time should be taken from pytest-mozwebqa via page.py's timeout property.
  • Single quotes (') should be used instead of double (") throughout.
  • Methods should have a single purpose.

Logic

  • Methods should not contain logic that depends on properties of the page. The logic and expectations should be within the test, and adding this to the page object could guard your tests against genuine failures.
# Good
def click_login(self)
    self.selenium.find_element(*self._login_locator).click()

# Bad
def click_login(self)
    if not self.is_user_logged_in:
        self.selenium.find_element(*self._login_locator).click()
    else:
        pass

Locators

  • Locator variables should be prefixed with _ to show that it is private.
  • Variables should be descriptive of the area and not clash with any properties.
  • Should have a suffix of _locator.
  • Accessing locators should be done through a property or method as this keeps the locator as read-only.
@property
def search_term(self):
    return self.selenium.find_element(*self._search_box_locator).value
  • We should use locators in the following order of preference (there will be exceptions):
    • ID
    • Name
    • Class name
    • CSS selector
    • XPath
  • CSS locators should use whitespace for readability when using direct descendants.
# Good
_my_locator = "css=#content > p > a"
    
# Bad
_my_locator = "css=#content>p>a"
  • Use Python tuples to define locators:
# Good
_my_locator = (By.ID, "content")

Actions

  • Methods that perform actions on the page should indicate the action in the method name.
# Good
def click_report_with_length(length)

# Bad
def report_length(length)
  • Actions should wait for the appropriate action to complete. This could be an implicit or explicit wait. For example, clicking a login button might explicitly wait for a username field to be visible.

Advanced: Page Regions

In some circumstances, for example where a header/navigation is common across the website, we will use a page region. The page region is a child class of the base Page object, which is inherited by all page objects. This means that the navigation can be reached from any page object and herein lies the DRY!

A brief example:

class BasePage(Page):

    @property
    def header(self):
        return BasePage.HeaderRegion(self.testsetup)
    
    class HeaderRegion(Page):

        _login_link = (By.ID, "home")

        @def click_login(self):
            self.selenium.find_element(*self._login_link).click()

Referring to this page region with a property makes it very readable and concise from within the test. Clicking login during a test would be performed like this:

my_page.header.click_login()

Another example where this might be used is on a search results page, the page region being the search results element.

Tests

  • Module names should be called test_ and then behavioral areas.
   test_search.py
  • Test method signature should include mozwebqa to use pytest-mozwebqa plugin.
def test_example(self, mozwebqa):
  • Test method names should always show the intent of the test case.
# Good
def test_that_advanced_search_does_not_find_item(self, mozwebqa):

# Bad
def test_advanced_search(self, mozwebqa):

Assertions

  • Tests should handle the asserts -- not the page objects.
  • Tests should use Python's native assert statement.
    • Note that this is a change from our previous standard of using the UnittestZero package.
  • When doing equivalency assertions, put the expected value first, followed by the actual value, for example:
assert 'expected' == 'actual'  # good
assert 'actual' == 'expected'  # bad
  • When doing negative equivalency, use != and put the unexpected value first, followed by the actual value, for example:
assert 'unexpected' != 'actual'  # good
assert 'actual' != 'unexpected'  # bad
  • To directly cause a test to fail raise an AssertionError with an appropriate message, for example:
raise AssertionError('message')

Size of patches

To make sure that we can review your patch as quickly and efficiently as possibly we would like patches to have a single test in them and the necessary changes to the page objects. This also limits the chances of merge conflicts later.

Using new and old standards together

As we (and Selenium and automation) develop more knowledge some projects might fall behind the standards in this style guide. It can be tempting to want to fix all of the outdated style but in order to keep patches/pulls small (see above!) we are happy to have new and old standards of code sit side by side. As we regularly review and update tests the project will be brought completely up to our current standards.

Or if you prefer, log a GitHub issue to have a section of code addressed separately to the job you are doing.