近些年, 随着互联网的爆炸式增长, 各种Web开发框架及相关技术层出不穷,
Web应用变得越来越丰富。 在Web 开发过程中, 除了要编写必要的单元测是代码, 也要编写相应的功能测试代码, 以实现功能测试的自动化, 从而增加迭代的速度。

在这方面, Selenium Webdriver 无疑是首选。 本文讲述Selenium Webdriver 的及其相关概念, 及如何用其进行Web功能测试自动化的开发。

What is Selenium?

  • Selenium automates browsers. That is it!
  • What you do with that power is entirely up to you.
  • Primarily, it is for automating web applications for testing purposes, but is certainly not limited to just that.
  • Boring web-based administration tasks can (and should!)also be automated as well.
  • Selenium has the support of some of the largest browser vendors who have taken (or are taking) steps to make Selenium a native part of their browser. It is also the core technology in countless other browser automation tools, APIs and frameworks.

Selenium IDE

What is Selenium IDE

  • Selenium IDE is a Firefox plugin which records and plays back user interactions with the browser.
  • Use this to either create simple scripts or assist in exploratory testing.

Download & Install

  • Open Firefox web browser
  • Open this page: and download latest released version 2.5.0

Selenium Server

  • The Selenium Server is needed in order to run either Selenium RC style scripts or Remote Selenium Webdriver ones.
  • Download version 2.40.0
  • Run in cmd:
    $ java -jar selenium-server-standalone-2.x.x.jar
    

Other Firefox Plugins

  • Firebug
  • Firefinder for Firebug
  • Xpath Checker
  • Xpath Finder

How to use Selenium-RC

The Selenium-RC has been eliminated, So do not need to learn it!!!

Selenium Webdriver

  • The biggest change in Selenium recently has been the inclusion of the WebDriver API.
  • Driving a browser natively as a user would either locally or on a remote machine using the Selenium Server it marks a leap forward in terms of browser automation.
  • Selenium 1.0 + WebDriver = Selenium 2.0

Python-Selenium

  • Official API documentation is available here.
  • Selenium Python bindings provides a simple API to write functional/acceptance tests using Selenium WebDriver.
  • Through Selenium Python API you can access all functionalities of Selenium WebDriver in an intuitive way.
  • Selenium Python bindings provide a convenient API to access Selenium WebDrivers like Firefox, Ie, Chrome, Remote etc..
  • The current supported Python versions are 2.6, 2.7, 3.2 and 3.3.

Python-Selenium download & install

  • Download Python bindings for Selenium from the PyPI page.
  • easy_install:

    $ easy_install selenium
    
  • pip to install the bindings:

    $ pip install selenium
    

Selenium Webdriver Simple Usage

from selenium import webdriver
from selenium.webdriver.common.keys import Keys


driver = webdriver.Firefox()

driver.get("http://www.python.org")
assert "Python" in driver.title

elem = driver.find_element_by_name("q")
elem.send_keys("selenium")
elem.send_keys(Keys.RETURN)
assert "Google" in driver.title

driver.close()

NOTE: The above script can be saved into a file
(eg:- python_org_search.py), then it can be run like this: python python_org_search.py

Using Selenium to write tests

  • The selenium package itself doesn't provide a testing tool/framework.
  • You can write test cases using Python's unittest module.
  • The other choices as a tool/framework are py.test and nose.
  • This is a test for python.org search functionality.
    import unittest
    from selenium import webdriver
    from selenium.webdriver.common.keys import Keys
    
    class PythonOrgSearch(unittest.TestCase):
    
        def setUp(self):
            self.driver = webdriver.Firefox()
    
        def test_search_in_python_org(self):
            driver = self.driver
            driver.get("http://www.python.org")
            self.assertIn("Python", driver.title)
            elem = driver.find_element_by_name("q")
            elem.send_keys("selenium")
            elem.send_keys(Keys.RETURN)
            self.assertIn("Google", driver.title)
    
        def tearDown(self):
            self.driver.close()
    
    if __name__ == "__main__":
        unittest.main()
    

Using Selenium with remote WebDriver

$ java -jar selenium-server-standalone-2.x.x.jar

While running the Selenium server, you could see a message looks like this: 15:43:07.541 INFO - RemoteWebDriver instances should connect to: http://127.0.0.1:4444/wd/hub

The above line says that, you can use this URL for connecting to remote WebDriver. Here are some examples:

from selenium.webdriver.common.desired_capabilities import DesiredCapabilities


    driver = webdriver.Remote(
            command_executor="http://127.0.0.1:4444/wd/hub",
            desired_capabilities=DesiredCapabilities.CHROME)
    driver = webdriver.Remote(
            command_executor="http://127.0.0.1:4444/wd/hub",
            desired_capabilities=DesiredCapabilities.OPERA)

Interacting with the page

For example, given an element defined as:

<input type="text" name="passwd" id="passwd-id" />

you could find it using any of:

element = driver.find_element_by_id("passwd-id")
element = driver.find_element_by_name("passwd")
element = driver.find_element_by_xpath("//input[@id='passwd-id']")

Filling in forms

element = driver.find_element_by_xpath("//select[@name='name']")
all_options = element.find_elements_by_tag_name("option")

for option in all_options:
    print "Value is: %s" % option.get_attribute("value")
    option.click()

WebDriver's support classes include one called “Select”, which provides useful methods for interacting with these.

select = Select(driver.find_element_by_name("name"))
select.select_by_index(index)
select.select_by_visible_text("text")
select.select_by_value(value)

WebDriver also provides features for deselecting all the selected options:

select = Select(driver.find_element_by_id("id"))
select.deselect_all()

This will deselect all OPTIONs from the first SELECT on the page.

Suppose in a test, we need the list of all default selected options, Select class provides a property method that returns a list:

select = Select(driver.find_element_by_xpath("xpath"))
all_selected_options = select.all_selected_options

To get all available options:

options = select.options

Once you've finished filling out the form, you probably want to submit it. One way to do this would be to find the “submit” button and click it:

# Assume the button has the ID "submit" :)
driver.find_element_by_id("submit").click()

Alternatively, WebDriver has the convenience method “submit” on every element. If you call this on an element within a form, WebDriver will walk up the DOM until it finds the enclosing form and then calls submit on that. If the element isn't in a form, then the NoSuchElementException will be raised:

element.submit()

Drag and drop

You can use drag and drop, either moving an element by a certain amount, or on to another element:

from selenium.webdriver import ActionChains


element = driver.find_element_by_name("source")
target = driver.find_element_by_name("target")
action_chains = ActionChains(driver)
action_chains.drag_and_drop(element, target)

Moving between windows and frames

It's rare for a modern web application not to have any frames or to be constrained to a single window. WebDriver supports moving between named windows using the “switch_to_window” method:

driver.switch_to_window("windowName")

All calls to driver will now be interpreted as being directed to the particular window.

But how do you know the window's name? Take a look at the javascript or link that opened it:

<a href="somewhere.html" target="windowName">Click here to open a new window</a>

Alternatively, you can pass a “window handle” to the “switch_to_window()” method. Knowing this, it's possible to iterate over every open window like so:

for handle in driver.window_handles:
    driver.switch_to_window(handle)

You can also swing from frame to frame (or into iframes):

driver.switch_to_frame("frameName")

It's possible to access subframes by separating the path with a dot, and you can specify the frame by its index too. That is:

driver.switch_to_frame("frameName.0.child")

would go to the frame named “child” of the first subframe of the frame called “frameName”. All frames are evaluated as if from top. Once we are done with working on frames, we will have to come back to the parent frame which can be done using:

driver.switch_to_default_content()

Popup dialogs

alert = driver.switch_to_alert()

Navigation: history and location

navigation is a useful task.

To navigate to a page, you can use get method:

driver.get("http://www.example.com")

To move backwards and forwards in your browser's history:

driver.forward()
driver.back()

Please be aware that this functionality depends entirely on the underlying driver. It's just possible that something unexpected may happen when you call these methods if you're used to the behaviour of one browser over another.

Methods of Locating Elements

There are vaious strategies to locate elements in a page. You can use the most appropriate one for your case.

Selenium provides the following methods to locate elements in a page:

  • find_element_by_id
  • find_element_by_name
  • find_element_by_xpath
  • find_element_by_link_text
  • find_element_by_partial_link_text
  • find_element_by_tag_name
  • find_element_by_class_name
  • find_element_by_css_selector

To find multiple elements (these methods will return a list):

  • find_elements_by_name
  • find_elements_by_xpath
  • find_elements_by_link_text
  • find_elements_by_partial_link_text
  • find_elements_by_tag_name
  • find_elements_by_class_name
  • find_elements_by_css_selector

Locating by Id

  • Use this when you know id attribute of an element.
  • With this strategy, the first element with the id attribute value matching the location will be returned.
  • If no element has a matching id attribute, a NoSuchElementException will be raised.
    login_form = driver.find_element_by_id("loginForm")
    

Note: "ID" is unique in one web page!

Locating by Name

  • Use this when you know name attribute of an element. With this strategy, the first element with the name attribute value matching the location will be returned.
  • If no element has a matching name attribute, a NoSuchElementException will be raised.
    username = driver.find_element_by_name("username")
    password = driver.find_element_by_name("password")
    

Locating Hyperlinks by Link Text

  • Use this when you know link text used within an anchor tag. With this strategy, the first element with the link text value matching the location will be returned.
  • If no element has a matching link text attribute, a NoSuchElementException will be raised.
    continue_link = driver.find_element_by_link_text("Continue")
    continue_link = driver.find_element_by_partial_link_text("Conti")
    

Locating by XPath

  • XPath is the language used for locating nodes in an XML document.
  • As HTML can be an implementation of XML (XHTML), Selenium users can leverage this powerful language to target elements in their web applications.
  • One of the main reasons for using XPath is when you don't have a suitable id or name attribute for the element you wish to locate.
  • You can use XPath to either locate the element in absolute terms (not advised), or relative to an element that does have an id or name attribute.
  • XPath locators can also be used to specify elements via attributes other than id and name.
    login_form = driver.find_element_by_xpath("/html/body/form[1]")
    login_form = driver.find_element_by_xpath("//form[1]")
    login_form = driver.find_element_by_xpath("//form[@id='loginForm']")
    

These examples cover some basics, but in order to learn more, the following references are recommended:

CSS Locator

  • Cascading Style Sheets (CSS).
  • Selenium WebDriver uses same principles of CSS selectors to locate elements in DOM.
  • This is a much faster and more reliable way to locate the elements when compared with XPaths.

For instance, consider this page source:

<html>
<body>
    <p class="content">Site content goes here.</p>
</body>
<html>

The “p” element can be located like this:

content = driver.find_element_by_css_selector('p.content')

login_form = driver.find_element_by_css_selector("body form>input")

Explicit Waits

An explicit waits is code you define to wait for a certain condition to occur before proceeding further in the code. The worst case of this is time.sleep(), which sets the condition to an exact time period to wait. There are some convenience methods provided that help you write code that will wait only as long as required. WebDriverWait in combination with ExpectedCondition is one way this can be accomplished.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


driver = webdriver.Firefox()
driver.get("http://somedomain/url_that_delays_loading")
try:
    element = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "myDynamicElement")))
finally:
    driver.quit()

This waits up to 10 seconds before throwing a TimeoutException or if it finds the element will return it in 0 - 10 seconds.

WebDriverWait by default calls the ExpectedCondition every 500 milliseconds until it returns successfully. A successful return is for ExpectedCondition type is Boolean return true or not null return value for all other ExpectedCondition types.

Expected Conditions

There are some common conditions that are frequently come across when automating web browsers. Listed below are Implementations of each. Selenium Python binding provides some convienence methods so you don't have to code an expected_condition class yourself or create your own utility package for them.

  • title_is
  • title_contains
  • presence_of_element_located
  • visibility_of_element_located
  • visibility_of
  • presence_of_all_elements_located
  • text_to_be_present_in_element
  • text_to_be_present_in_element_value
  • frame_to_be_available_and_switch_to_it
  • invisibility_of_element_located
  • element_to_be_clickable - it is Displayed and Enabled.
  • staleness_of
  • element_to_be_selected
  • element_located_to_be_selected
  • element_selection_state_to_be
  • element_located_selection_state_to_be
  • alert_is_present
    from selenium.webdriver.support import expected_conditions as EC
    
    wait = WebDriverWait(driver, 10)
    element = wait.until(EC.element_to_be_clickable((By.ID,'someid')))
    

The expected_conditions module contains a set of predefined conditions to use with WebDriverWait.

Implicit Waits

An implicit wait is to tell WebDriver to poll the DOM for a certain amount of time when trying to find an element or elements if they are not immediately available. The default setting is 0.
Once set, the implicit wait is set for the life of the WebDriver object instance.

from selenium import webdriver


ff = webdriver.Firefox()
ff.implicitly_wait(10) # seconds
ff.get("http://somedomain/url_that_delays_loading")
myDynamicElement = ff.find_element_by_id("myDynamicElement")

Using custom Firefox profile

from selenium import webdriver


ff_profile = webdriver.FirefoxProfile("/home/ff_profile_dir")
profile.set_preference('browser.download.folderList', 2)
profile.set_preference('browser.download.manager.showWhenStarting', False)

driver = webdriver.Firefox(ff_profile)

Using a Proxy

from selenium import webdriver


PROXY = "localhost:8080"

# Create a copy of desired capabilities object.
desired_capabilities = webdriver.DesiredCapabilities.INTERNETEXPLORER.copy()
# Change the proxy properties of that copy.
desired_capabilities['proxy'] = {
                                "httpProxy":PROXY,
                                "ftpProxy":PROXY,
                                "sslProxy":PROXY,
                                "noProxy":None,
                                "proxyType":"MANUAL",
                                "class":"org.openqa.selenium.Proxy",
                                "autodetect":False}

driver = webdriver.Remote("http://localhost:4444/wd/hub", desired_capabilities)

Using other web browser webdriver

NOTE:You need to download the special webdriver Such as using ChromeDriver Firstly, download the latest chromedriver from download page. Then unzip the file:

You should see a chromedriver executable. Now you can create an instance of Chrome WebDriver like this:

driver = webdriver.Chrome(executable_path="/path/to/chromedriver")

The rest of the example should work as given in other other documentation.

Selenium Grid

  • Firstly, Run the hub

    $ java -jar selenium-server-standalone.jar -role hub
    
  • Secondly, Run the node (with Firefox browser)

    $ java -jar selenium-server-standalone.jar -role node \
      -hub http://selenium-srv-fqdn:4444/grid/register -browser browserName=firefox
    
  • Code

    from selenium import webdriver
    
    desired_capabilities = {'browserName': 'firefox'}
    ff = webdriver.Remote('http://selenium-srv-fqdn:4444/wd/hub', desired_capabilities)
    
  • More information about Selenium Grid you can refer to this page

Test design

Test design with page object

Page Object (PO)

A POs simply model areas of the app's UI as objects within the test code. This reduces the amount of duplicated code and if the UI changes, the fix need only be applied in one place.
There is clean separation between test code and page specific code.

Summary

  • the public methods represent the services that the page
  • offers try not to expose the internals of the page generally don't
  • make assertions (except initial verification) methods return other POs
  • need not represent an entire page different results for the same
  • action are modelled as different methods

Simple login test (without design)

from selenium import webdriver


def login_test():
    ff = webdriver.Firefox()
    ff.get('http://fqdn/login')

    username = ff.find_element_by_id('user_name')
    password = ff.find_element_by_id('password')
    submit_btn = ff.find_element_by_id('submit_button')

    username.send_keys('admin')
    password.send_keys('123456')
    submit_btn.click()

    assert 'Welcome' in ff.title

Pros: simple, easy to understand
Cons: not reusable (against DRY principle), hard to maintain

Page Object (PO) example

Instead of this:

def login_test():
    ff = webdriver.Firefox()
    ff.get('http://example.com/login')

    username = ff.find_element_by_id('user_name')
    password = ff.find_element_by_id('password')
    submit_btn = ff.find_element_by_id('submit_button')
    ...
    ...

Use this:

def login_test():
    ff = webdriver.Firefox()
    ff.get('http://example.com/login')

    login_page = LoginPage(ff)
    login_page.login_user('admin', '123456')
    assert 'Welcome' in ff.title

Test design with UI mapping

UI mapping

A UI map is a mechanism that stores all the locators for a test suite in one place for easy modification when identifiers or paths to UI elements change.

Pros

  • centralized location for UI objects instead of having them scattered throughout the script (better maintenance)
  • cryptic HTML identifiers and names can be given more human-readable names (better readability)

How to locate elements on page:

id, xpath, link text, partial link text, name, tag name, class name, css selector driver.find_element_by_... driver.find_elements_by_... driver.find_element(by=, value=)

UI mapping example

Instead of this:

def login_test():
    ...
    ...
    username = ff.find_element_by_xpath('//path[1]/to[2]/username[@type="element"]')
    password = ff.find_element_by_xpath('//path[2]/to[2]/password[@type="element"]')

Use this

login_page_map = Enum(
        username = '//path[1]/to[2]/imput[@title="Username"]',
        password = '//path[2]/to[2]/input[@type="password"]',)

def login_test():
    ...
    ...
    username = ff.find_element_by_xpath(login_page_map.username)
    password = ff.find_element_by_xpath(login_page_map.password)

Example - Page Object with UI map

from selenium.webdriver.common.by import By


login_page_map = Enum(
        username = (By.XPATH, '//path[1]/to[2]/imput[@title="Username"]'),
        password = (By.XPATH, '//path[2]/to[2]/input[@type="password"]'),)

class LoginPage(PageObject):
    def __init__(self, driver):
        self._driver = driver
        assert 'Login' in self._driver.title

username = self._driver.find_element(login_page_map.username)
password = self._driver.find_element(login_page_map.password)

def login_user(self, username, password):
    self.username.send_keys(username)
    self.password.send_keys(password)

Login test example - test itself

def login_test():
    ff = webdriver.Firefox()
    ff.get('http://example.com/login')
    login_page = LoginPage(ff)
    login_page.login_user('admin', '123456')
    assert 'Welcome' in ff.title

UI map is done via pageElement decorator

Instead of

class LoginPage(PageObject):
    username = self._driver.find_element_by_xpath(login_page_map.username)
    password = self._driver.find_element_by_xpath(login_page_map.password)

Use

class LoginPage(PageObject):

    @pageElement(login_page_map.username)
    def username(self):
        pass

    @pageElement(login_page_map.password)
    def password(self):
        pass

Learn more to

  • http://docs.seleniumhq.org/
  • https://selenium-python.readthedocs.org/


Comments

comments powered by Disqus