Python Web Automation (Behave)

Posted April 25, 2016

Selenium, Page Objects, Behave and Nose in Python

Introduction

Earlier, I wrote a post about using Selenium with Lettuce in a Python context. In this post, we will have a look at using Selenium WebDriver with Behave. Behave is very similar to Lettuce, in that it allows for tests to be written in a natural language style, but it does seem a bit simpler to use and setup. We will also make use of Nose for its assertions.

Installation

Python

Install Python 2.7.10. Please ensure that you allow the installer to update your PATH. As part of your installation, please also ensure that you install pip, which is a tool that allows easy management of any Python packages that you wish to use. Installers for versions prior to Python 2.7.9 will not have pip bundled, so if you do choose to use an earlier version, please ensure you manually install pip.

Ensure that you have successfully installed Python:

bash-3.2$ python --version  
Python 2.7.10

Ensure that you have successfully installed pip:

bash-3.2$ pip --version
pip 6.1.1 from /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages (python 2.7)

You can now use the following commands to install the Selenium, Behave and Nose packages:

bash-3.2$ pip install selenium
bash-3.2$ pip install behave
bash-3.2$ pip install nose
Sublime Text

Install Sublime Text 3.

Firefox

Install Firefox.

Initial Setup

We are going to write our first automated test against PyPI. Create a new directory for your test automation project, and open that directory in Sublime Text 3.

Basic Selenium WebDriver Usage

Create a new file called sample-test.py and place the following contents:

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Firefox()
driver.get("https://pypi.python.org/pypi")
driver.find_element(By.ID, "term").send_keys("Selenium")
driver.find_element(By.ID, "submit").click()
driver.quit()

Run the test using python sample-test.py and observe that Firefox is started:

bash-3.2$ python sample-test.py
Using Python’s Unit Testing Framework (unittest)

Here we will start to make use of nose. The nose library extends unittest to act as a test runner, which provides more meaningful results at execution.

import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By

class SampleTest(unittest.TestCase):

    def pypi_test(self):
        self.driver = webdriver.Firefox()
        self.driver.get("https://pypi.python.org/pypi")
        self.driver.find_element(By.ID, "term").send_keys("Selenium")
        self.driver.find_element(By.ID, "submit").click()
        self.driver.quit()

Run the test using nosetests or nosetests sample-test.py:

bash-3.2$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 4.294s

OK

Let’s now update the test to include unittest’s setUp() and tearDown() methods. Update sample-test.py with the following contents:

import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By

class SampleTest(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Firefox()

    def pypi_test(self):
        self.driver.get("https://pypi.python.org/pypi")
        self.driver.find_element(By.ID, "term").send_keys("Selenium")
        self.driver.find_element(By.ID, "submit").click()

    def tearDown(self):
        self.driver.quit()

Run the test using nosetests or nosetests sample-test.py:

bash-3.2$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 4.563s

OK

Using Behave

Create a folder structure similar to this:

pypi_automated_tests/
  features/
    __init__.py
    pages/
        __init__.py
    steps/
        __init__.py

The __init__.py files can be left empty, but will allow for the containing directories to recognised as Python packages. To make use of Behave, we will first have to create a new file pypi_automated_tests/features/environment.py. This file can be used by Behave to define the functions that run before_all() or after_all() events in your test, which is very similar to unittest’s setUp() and tearDown() methods. In this file, place the following code:

from selenium import webdriver

def before_all(context):
    context.browser = webdriver.Firefox()
    # context.browser = webdriver.Chrome() if you have set chromedriver in your PATH
    context.browser.set_page_load_timeout(10)
    context.browser.implicitly_wait(10)
    context.browser.maximize_window()

def after_all(context):
    context.browser.quit()

Create a new file pypi_automated_tests/features/search.feature:

Feature: Search

  Scenario: Search PyPI
    Given I navigate to the PyPi Home page
    When I search for "behave"
    Then I am taken to the PyPi Search Results page
    And I see a search result "behave 1.2.5"

Now, let’s create a new file pypi_automated_tests/features/steps/search_steps.py. In this file, let’s first define a shell for our steps:

from nose.tools import assert_equal, assert_true
from selenium.webdriver.common.by import By

@step('I navigate to the PyPi Home page')
def step_impl(context):
    context.browser.get("https://pypi.python.org/pypi")
    assert_equal(context.browser.title, "PyPI - the Python Package Index : Python Package Index")

@step('I search for "{search_term}"')
def step_impl(context, search_term):
    context.browser.find_element(By.ID, "term").send_keys(search_term)
    context.browser.find_element(By.ID, "submit").click()

@step('I am taken to the PyPi Search Results page')
def step_impl(context):
    assert_equal(context.browser.title, "Index of Packages Matching 'behave' : Python Package Index")

@step('I see a search result "{search_result}"')
def step_impl(context, search_result):
    assert_true(context.browser.find_element(By.LINK_TEXT, search_result))

If you run behave from pypi_automated_tests/ you will now see that the test was run successfully:

bash-3.2$ behave
Feature: Search # features/search.feature:1

  Scenario: Search PyPI                             # features/search.feature:3
    Given I navigate to the PyPi Home page          # features/steps/search_steps.py:4 1.498s
    When I search for "behave"                      # features/steps/search_steps.py:9 2.556s
    Then I am taken to the PyPi Search Results page # features/steps/search_steps.py:14 0.011s
    And I see a search result "behave 1.2.5"        # features/steps/search_steps.py:18 0.120s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
4 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m4.184s

Using Page Objects

To make use of Page Objects, let’s first create pypi_automated_tests/features/browser.py. Let’s move our WebDriver functionality from environment.py to browser.py (which also makes it accessible to our Page Objects):

from selenium import webdriver

class Browser(object):

    driver = webdriver.Firefox()
    # driver = webdriver.Chrome() if you have set chromedriver in your PATH
    driver.implicitly_wait(30)
    driver.set_page_load_timeout(30)
    driver.maximize_window()

    def close(context):
        context.driver.close()

You can now also update environment.py to look like this:

from selenium import webdriver
from browser import Browser

def before_all(context):
    context.browser = Browser()

def after_all(context):
    context.browser.close()

Now move the functionality that resided in pypi_automated_tests/features/steps/search_steps.py to two new files, pypi_automated_tests/features/pages/home_page.py and pypi_automated_tests/features/pages/search_results_page.py. Firstly, in pypi_automated_tests/features/pages/home_page.py make the following updates:

from selenium.webdriver.common.by import By
from browser import Browser

class HomePageLocator(object):
    # Home Page Locators

    HEADER_TEXT = (By.XPATH, "//h1")
    SEARCH_FIELD = (By.ID, "term")
    SUBMIT_BUTTON = (By.ID, "submit")


class HomePage(Browser):
    # Home Page Actions

    def fill(self, text, *locator):
        self.driver.find_element(*locator).send_keys(text)

    def click_element(self, *locator):
        self.driver.find_element(*locator).click()

    def navigate(self, address):
        self.driver.get(address)

    def get_page_title(self):
        return self.driver.title

    def search(self, search_term):
        self.fill(search_term, *HomePageLocator.SEARCH_FIELD)
        self.click_element(*HomePageLocator.SUBMIT_BUTTON)

Secondly, in pypi_automated_tests/features/pages/search_results.py make the following updates:

from selenium.webdriver.common.by import By
from browser import Browser

class SearchResultsPageLocator(object):
    # Search Results Page Locators

    HEADER_TEXT = (By.XPATH, "//h1")


class SearchResultsPage(Browser):
    # Search Results Page Actions

    def get_element(self, *locator):
        return self.driver.find_element(*locator)

    def get_page_title(self):
        return self.driver.title

    def find_search_result(self, search_result):
        return self.get_element(By.LINK_TEXT, search_result)

You can now again update environment.py to initialise the 2 new Page Objects:

from selenium import webdriver
from browser import Browser

def before_all(context):
    context.browser = Browser()
    context.home_page = HomePage()
    context.search_results_page = SearchResultsPage()

def after_all(context):
    context.browser.close()

Now, let’s update pypi_automated_tests/features/steps/search_steps.py to make use of the newly added Page Objects:

from nose.tools import assert_equal, assert_true
from selenium.webdriver.common.by import By

@step('I navigate to the PyPi Home page')
def step_impl(context):
    context.home_page.navigate("https://pypi.python.org/pypi")
    assert_equal(context.home_page.get_page_title(), "PyPI - the Python Package Index : Python Package Index")

@step('I search for "{search_term}"')
def step_impl(context, search_term):
    context.home_page.search(search_term)

@step('I am taken to the PyPi Search Results page')
def step_impl(context):
    assert_equal(context.search_results_page.get_page_title(), "Index of Packages Matching 'behave' : Python Package Index")

@step('I see a search result "{search_result}"')
def step_impl(context, search_result):
    assert_true(context.search_results_page.find_search_result(search_result))

Finally, in our pypi_automated_tests/features/environment.py we will need to make these Page Objects avaiable by making the following updates:

from selenium import webdriver
from browser import Browser
from pages.home_page import HomePage
from pages.search_results_page import SearchResultsPage

def before_all(context):
    context.browser = Browser()
    context.home_page = HomePage()
    context.search_results_page = SearchResultsPage()

def after_all(context):
    context.browser.close()

Execution

You can now run behave from pypi_automated_tests/, and you should get the following successful results:

bash-3.2$ behave
Feature: Search # features/search.feature:1

  Scenario: Search PyPI                             # features/search.feature:3
    Given I navigate to the PyPi Home page          # features/steps/search_steps.py:4 1.807s
    When I search for "behave"                      # features/steps/search_steps.py:9 5.057s
    Then I am taken to the PyPi Search Results page # features/steps/search_steps.py:13 0.014s
    And I see a search result "behave 1.2.5"        # features/steps/search_steps.py:17 0.142s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
4 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m7.020s

Full Example

https://github.com/the-creative-tester/python-behave-web-browser-automation-example