Let’s say you have a task to automate the testing of an application. Where should you start? The first step is to choose an approach to test automation, which will be the basis for your test development. When you are searching for possible options, you will find out that there are many of them, like unit testing, test-driven development, keyword-driven development, behavior-driven development and so on. In this article, we are going to talk about one of the most popular approaches to test automation – BDD or behavior-driven development. Follow the examples here on GitHub.

Explaining BDD

I suspect you might have a question here: “There is nothing about testing in the technique’s name, so how it can be used for testing?”. BDD originates from the test-driven development technique (TDD). This technique defines that before any functionality is implemented, tests should be created first. Usually TDD is useful for short term iterations when you need to keep your functionality safe from regression for a single unit that is under development.

But what about integration with other modules? Integration tests are more complex and require more knowledge and time to implement them. As this point when we need to turn our focus to BDD, where instead of module tests, behavior tests are automated.

What are considered as “behavior tests”? Behavior tests come from specification and business requirements. Business stakeholders, QA engineers, analysts, application and test developers work together to identify the correct flow and test it. With this approach, every new requirement and functionality can be added so they are covered by tests in the same iteration. Seems promising!

BDD Scenarios in Gherkin

Let’s have a look at BDD in action. In python, the behave framework is a great implementation of that technique. Scenarios in behave are written using the Gherkin syntax. A simple scenario in Gherkin looks like this:

Feature:  User authorization and authorization
Scenario: The user can log into the system
Given The user is registered in the system
When The user enters a login
And enters a password
Then the user is logged in
  • Feature keyword – describes which part of the functionality scenarios are being created for.
  • Scenario keyword – is used to describe the test case title.
  • Given keyword – describes pre-conditions required to complete the scenario.
  • When keyword – is used to describe the scenario’s steps.
  • Then keyword – describes the expected result of the scenario.
  • And keyword – can be used for Given, When and Then keywords to describe additional steps.

Our BDD Scenario

Let’s automate a scenario for http://blazedemo.com/ and verify if the user can find flights from Paris to London. The scenario will look like this:

Feature: The user can book available flights 
Scenario: The user can find a flight from Paris to London
Given the user is on the search page
When the user selects Paris as a departure city
And the user selects London as a destination city
And clicks on the Find Flights button
Then flights are presented on the search result page

Prerequisites and Installations

To automate the test, we will need:

1. Python 2.7.14 or above. You can download it from here. There are two major versions of python nowadays: 2.7.14 and 3.6.4. Code snippets in the blog post will be given for version 2.7.14. If there is any difference for version 3.6.4, a note will be made with appropriate changes to version 3.6.4. It’s up to the reader to choose which version to install.

2. To install python package manager (pip). It can be downloaded from its download page. All further installations in the blog post will make use of pip so it’s highly recommended to install it.

3. A development environment. The PyCharm Community edition will be used in this blog post. You can download it from the Pycharm website. You can use any IDE of your choice since code snippets are not IDE dependent.

Now, let’s create our project.

4. Create the project in PyCharm IDE with File -> New Project.

BDD testing, Selenium, behave

5. Specify the location for the project (the last part of the path will be the project’s name).

When developing a python application, it’s a good practice to isolate its dependencies from others. By doing this, you can be sure that you are using the right version of the library in case there are multiple versions of it in your PYTHON_PATH. (The PYTHON_PATH variable is used in python applications to find any dependency that is declared via import statement in python modules).

To do this, you need to install the virtual environment.

6. Install the Virtual Environment tool with the command pip install virtualenv in the prompt.

7. In the project root directory, create the environment with virtualenv BLAZEDEMO in the prompt where BLAZEDEMO is the environment’s name.

You will notice that in the project root you have a new directory created – BLAZEDEMO. This is the folder of your virtual environment. Once activated, the python interpreter will be used to switch to the one available in the virtual environment. Also, all packages and dependencies will be installed within the virtual environment and will not be available outside of it.

8. To activate it, in POSIX systems run the source bin/activate command from the environment root folder. In Windows systems, go to environment folder -> Scripts and execute activate.bat from the prompt.

If the environment is activated, your prompt will be prefixed with the environment’s name as below:

installing behave, BDD, Selenium

Now, we can install all the packages that are required to automate our first BDD scenario.

9. Install ‘behave’ by executing ‘pip install behave’ in the prompt.

10. Since our application is a web application, we will need a tool that helps us interact with the Graphical User Interface. Selenium will make a perfect match here since it enables interacting with any element on a web page just like a user would: clicking on buttons, typing into text fields and so on. Additionally, Selenium has wide support of all popular browsers, and good and full documentation, which make it an easy-to-use tool. To install it, just execute ‘pip install selenium’ in the prompt.

Creating our Selenium Scenario in behave

In behave, all scenarios are kept in feature files that should be put in the features directory.

Let’s create flight_search.feature and put the scenario we created earlier into it.

selenium scenarios in behave

All scenario steps must have an implementation. But we need to take care of a few things before:

First, define a browser to run scenarios in. As Selenium only defines the interface to interact with a browser and a few useful tools, the actual implementation is performed by WebDriver. Each browser has its own implementation of WebDriver. Download all the implementations you need from the download page, i.e. for Chrome browser you will need ChromeDriver. Select the appropriate version depending on your operating system.

Waiting for Elements with WebDriverWait

Once WebDriver is downloaded, we need to define the way to access it. For web applications, there is one thing we need to take care of: since elements are not loaded simultaneously, we need to wait until they become available. Web driver itself doesn’t wait for elements to be accessible – it tries to get them immediately. To bypass this, Selenium has a WebDriverWait class that explicitly waits for elements by different conditions. Create a new python directory web and add web.py there.

waiting for webelements in bdd scenarios in Selenium

Here is the web.py script for waiting for elements:

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


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


class Web(object):
    __TIMEOUT = 10

    def __init__(self, web_driver):
        super(Web, self).__init__()  # in python 3.6 you can just call super().__init__()
        self._web_driver_wait = WebDriverWait(web_driver, Web.__TIMEOUT)
        self._web_driver = web_driver

    def open(self, url):
        self._web_driver.get(url)

    def find_by_xpath(self, xpath):
        return self._web_driver_wait.until(EC.visibility_of_element_located((By.XPATH, xpath)))

    def finds_by_xpath(self, xpath):
        return self._web_driver_wait.until(EC.presence_of_all_elements_located((By.XPATH, xpath)))
 

In the constructor, there is the instance variable self._web_driver_wait that references to the instance of the WebDriverWait class. In the method find_by_xpath we use self._web_driver_wait to wait until the element, which can be found by xpath, becomes visible on a web page. The same goes for the method finds_by_xpath with the difference that it searches for multiple elements to be present on a web page. The method ‘open’ simply opens a web page by url.

Browser Initialization

As the final step we need to take care of when a Web instance should be initialized. We need to remember that with every instance of the web driver there is a new instance of the corresponding web browser. I believe you don’t want to start a new browser every time for every single test; except rare cases when you need to have a clean browser, without cache or any local stored data. This behavior can be achieved in two ways:

  1. With the usage of fixtures. If you are familiar with a testing framework like pytest, you already know what fixtures are. Fixtures are functions, whose main purpose is to execute initialization, configuration and cleanup code.
  2. With environment functions in the environment.py file. Within the environment methods can be executed before and after each test, scenario, feature, or tag, or the whole execution.

To understand how these two approaches work, we need to be familiar with one important behave feature – Context. Think about a context as an object that can store any user-defined data, together with behave-defined data, in context attributes.

Depending on the level of execution: feature, scenario or test, behave will add specific attributes to context, such as: feature, store the currently executed feature, scenario, store the currently executed scenario and so on. Also, you can access the result of a test, test parameters and the environment. You can browse all the possible behave-based attributes in Context in the behave API documentation.

Browser Initialization with Fixtures

Ok, let’s start with fixture implementation in the fixtures.py file

from selenium import webdriver
from web_driver.web import Web



def browser_chrome(context, timeout=30, **kwargs):
    browser = webdriver.Chrome("C:/chromedriver.exe")
    web = Web(browser)
    context.web = web
    yield context.web
    browser.quit()

Here, we have browser_chrome(context, timeout=30, **kwargs) function that will be used as a fixture to start a web browser.

This function starts a webdriver instance that starts a Chrome browser. Then we create the instance of the Web class to access web elements on webpages. Later, we create a new attribute in context – web, that can be referenced further in our steps implementations to access the Web instance. By using yield and browser.quit() in the next line, we are making sure that the browser will be closed after all the tests that use browser_chrome are complete.

Let’s see this in action and provide the implementation for the steps in the scenario we defined earlier in the flight_search.feature file. In the features directory of the project create a new python directory, steps, and add flight_search_steps.py there.

implementing selenium in behave, bdd testing
from behave import given, when, then
from behave.log_capture import capture


@given("the user is on search page")
def user_on_search_page(context):
    context.web.open("http://blazedemo.com/")


@when("user selects Paris as departure city")
def user_select_departure_city(context):
    context.web.find_by_xpath("//select[@name='fromPort']/option[text()='Paris']").click()

@when("user selects London as destination city")
def user_select_destination_city(context):
    context.web.find_by_xpath("//select[@name='toPort']/option[text()='London']").click()

@when("clicks on Find Flights button")
def user_clicks_on_find_flights(context):
    context.web.find_by_xpath("//input[@type='submit']").click()

@then("flights are found")
def flights_are_found(context):
    elements = context.web.finds_by_xpath("//table/tbody/tr")
    assert len(elements) > 1

Every step that is mentioned in the flight_search.feature file has an implementation here. In every step we reference a web instance through the context. This is a convenient way to access a shared resource without taking care of the way it was initialized, isn’t it?

To apply a fixture, we need to define the behavior for the before_tag option in the features/environment.py file.

apply fixtures in selenium bdd scenarios
from behave import use_fixture

from fixtures import browser_chrome

def before_tag(context, tag):
    if tag == "fixture.browser.chrome":
        use_fixture(browser_chrome, context)

As you can see, if a tag equals “fixture.browser.chrome” then we execute the browser_chrome fixture by calling the use_fixture method. To try it out, in your project root directory, execute “behave” in the command line. The output should look like this:

fixtures in behave and selenium

There are two main disadvantages to loading a webdriver with this fixtures approach:

1. Fixtures have a scope that is defined by the scope of the tag “@fixture.*” If we are using @fixture in a scenario, then the web browser will be opened and closed for every scenario with the @fixture tag. The same happens if @fixture is applied to a feature. So, if we have multiple features, the browser will start and close for every feature. This is not good if we don’t want our features to be executed in parallel, but a great option otherwise, by the way.

2. We need to assign a @fixture tag to every scenario or feature that is supposed to have access to a web page. This is not a good option. Moreover, what if we want to switch to another browser? Do we need to go over all the features and modify @fixture.browser every time?! This issue can be solved by applying the fixture in before_all(context) function from environment.py. like this:

def before_all(context):
    if context.browser == "chrome":
        use_fixture(browser_chrome, context)

But it looks like we are searching for a solution for the issue with instruments that can be used themselves without fixtures. Let’s have a look at how.

Behave gives us the option to define configurations in configuration files that can be called either “.behaverc”, “behave.ini”, “setup.cfg” or “tox.ini” (your preference) and are located in one of three places:

  1. The current working directory (good for per-project settings),
  2. Your home directory ($HOME), or
  3. On Windows, in the %APPDATA% directory.

There are a number of configurations that are used by behave to setup your environment. But you can also use this file to identify your own environment variables, such as a browser.

define behave configurations

Besides the browser, we have stderr_capture and stdout_capture set to False. By default those parameters are set to True, which means that behave will not print any message to the console, or any other output you specified, if the test is not failed. Setting to False will force behave to print any output even if the test passed. This is a great option if you need to see what is going on in your tests.

Browser Initialization Environment Functions

Earlier in the blog post, I mentioned there were two ways to start a webdriver. The first one is to use fixtures. We have already defined how to do it. The second option is to use environment functions. Let’s see how to do it.

We will get access to a Web instance through the before_all environment function in environment.py file. To do that, at first create the web_source/web_factory.py file.

browser initializations, selenium, behave, bdd testing
from selenium import webdriver
from web.web import Web


def get_web(browser):
    if browser == "chrome":
        return Web(webdriver.Chrome("C:/chromedriver.exe"))

The code is quite simple and straightforward.

In the environment.py file for before_all(context):

from web_source.web_factory import get_web


def before_all(context):
    web = get_web(context.config.userdata['browser'])
    context.web = web

Here, in the code, we are getting the currently set browser from the “browser” variable defined in behave.ini in the [behave.userdata] section.

You can try out this code by executing “behave” in the command line.

This approach is more flexible since we don’t need to modify feature files to switch to another browser.

When to Use Fixtures

But then you can ask: when should I use fixtures? Fixtures are a good option if your initialization depends on the environment you are currently on. For example, if for the development environment you would like to setup a connection to one database, and for the production environment you would like to setup another connection. This can be achieved in the following way:

fixture_registry = {"develop": develop_database,
                    "production": production_database}


def before_tag(context, tag):
    if tag.startswith("environment"):
        use_fixture_by_tag(tag, context, fixture_registry)

For any feature and scenario tagged with @environment.develop or @environment.production, in the before_tag environment function, the appropriate fixture will be loaded and executed as defined in fixture_registry.

If you don’t know if you should be using fixtures or another approach, just ask yourself: will fixtures create more issues than they solve? If answer is no, that you can use it. Basically, almost everything that is configurable by tags, can be managed by fixtures. If you have tag : slow, you can increase your timeout and then revert it back for fast test cases and so on.

Implementing Parameterized Steps

In the feature from the features/flight_search.feature file we saw how to create a test with static data. But what if we want to search for a flight not only from Paris to London? For such purposes, you can use parameterized steps. Let’s have a look at the implementation:

The feature file will be modified to a new one:

Feature: The user can book available flights
  Scenario: The user can find a flight from Paris to London
    Given the user is on the search page
    When the user selects a departure city "Paris"
    And the user selects a destination city "London"
    And clicks on the Find Flights button
    Then flights are present on the search result page

And the steps implementation for choosing cities will be changed:

@when('the user select departure city "{city}"')
def user_select_departure_city(context, city):
    context.web.find_by_xpath("//select[@name='fromPort']/option[text()='{}']".format(city)).click()

@when('the user select destination city "{city}"')
def user_select_destination_city(context, city):
 context.web.find_by_xpath("//select[@name='toPort']/option[text()='{}']".format(city)).click()
 

As you can see now the required city is loaded from parameters.

Execution Commands

So far, we have executed our features using the simple command “behave”. But the behave framework suggests different options for execution. The most useful ones:

  • –include, –exclude –  to include or exclude features from test run.
  • –junit – if you would like to get a junit report. You can read more about JUnit on the official site.
  • -logging-level – to specify level of logging. By default INFO is selected, which means that all messages will be captured.
  • –logging-format – to define a format messages will be printed within.
  • –tags to filter scenarios that you would like to run. Only scenarios with the specified tag will be executed.

https://www.blazemeter.com/blog/using-the-behave-framework-for-selenium-bdd-testing-a-tutorial/