Sunday, September 6, 2015

Selenium + Python + unittest + nosetest + py.test. Start point.

In this article we are going to install and configure virtual environment in order to use it with such libraries like Selenium, nosetests, py.test and unittest.

So, to start development we need to install Python, after that Python package manager or pip. For Python we have an additional library which is useful for creating isolated Python environments: virtualenv.

Installation:

C:\Python27\Scripts>pip install -U virtualenv
Collecting virtualenv
Downloading virtualenv-13.1.2-py2.py3-none-any.whl (1.7MB)
100% |################################| 1.7MB 170kB/s
Installing collected packages: virtualenv
Successfully installed virtualenv-13.1.2

C:\Python27\Scripts>

After installing the virtual environment we should activate it and install Selenium WebDriver along with the frameworks that are going to be used for test development and execution. In my case it is unittest that includes into Python by default, nosetest and py.test.

Now, it is time to configure the environment.
First of all, we should create the required environment for our tutorial:

C:\Users\Mikhail\Documents\Development\virtual_environments>C:\Python27\Scripts\virtualenv.exe selenium_training
New python executable in selenium_training\Scripts\python.exe
Installing setuptools, pip, wheel...done.

C:\Users\Mikhail\Documents\Development\virtual_environments>

After that we should configure the new environment in Eclipse. Since we already have the project, we should go to the project's properties and click on the link 'Click here to configure an interpreter not listed' like in the picture below.



In the new window we should click the 'New' button, in the dialog you should enter a name for our environment and select Python's executable from our environment, like in the picture below:



After that, an additional window will be shown with the list of folders that should be included into Python's path:



Eclipse will process the new interpreter for several seconds



And after that we may choose this environment as the base for our project:



So, we created our environment and now it is the time to install nosetests, py.test and Selenium. Before the installation we should activate our virtual environment using activate command. Command source activate should be used in *nix based operation systems. After that we should deactivate it using deactivate command

C:\Users\Mikhail\Documents\Development\virtual_environments\selenium_training\Scripts>activate
(selenium_training) C:\Users\Mikhail\Documents\Development\virtual_environments\selenium_training\Scripts>pip install nose
Collecting nose
Downloading nose-1.3.7-py2-none-any.whl (154kB)
100% |################################| 155kB 435kB/s
Installing collected packages: nose
Successfully installed nose-1.3.7
(selenium_training) C:\Users\Mikhail\Documents\Development\virtual_environments\selenium_training\Scripts>pip install -U pytest
Collecting pytest
Downloading pytest-2.7.2-py2.py3-none-any.whl (127kB)
100% |################################| 131kB 440kB/s
Collecting py>=1.4.29 (from pytest)
Downloading py-1.4.30-py2.py3-none-any.whl (81kB)
100% |################################| 86kB 525kB/s
Collecting colorama (from pytest)
Downloading colorama-0.3.3.tar.gz
Building wheels for collected packages: colorama
Running setup.py bdist_wheel for colorama
Stored in directory: C:\Users\Mikhail\AppData\Local\pip\Cache\wheels\e3\24\8d\aec3db961cfbc8e939dc1843126548e7d479349f96659067e9

Successfully built colorama
Installing collected packages: py, colorama, pytest
Successfully installed colorama-0.3.3 py-1.4.30 pytest-2.7.2

(selenium_training) C:\Users\Mikhail\Documents\Development\virtual_environments\selenium_training\Scripts>pip install selenium
Collecting selenium
Downloading selenium-2.47.1-py2-none-any.whl (3.0MB)
100% |################################| 3.0MB 50kB/s
Installing collected packages: selenium
Successfully installed selenium-2.47.1

(selenium_training) C:\Users\Mikhail\Documents\Development\virtual_environments\selenium_training>deactivate
C:\Users\Mikhail\Documents\Development\virtual_environments\selenium_training>

The pip install command supports special requirements files. It is not a purpose of this article to describe how to do that – more information about this capability could be found here, as well as about the format of the requirement file – here

Selenium has integrated support only for FireFox. For other browsers we should download special executables. We expect that our tests will work in both Google Chrome and Mozilla FireFox. The special driver for Google Chrome is available here, the latest release is 2.19 (according to LATEST_RELEASE file). For Windows OS we should download a zip archive. This archive contains only one file chromedriver.exe. This file should be uploaded into directory from PATH environment variable.

The main purpose of this article is to demonstrate base usage of Selenium and several test frameworks on Python. A more detailed description will be discovered as part of several articles coming up next.

All the required modules and libraries have been installed and it is the time to make sure that all works fine.

Class for working with web-page

Both sites http://www.google.com and http://www.yandex.com have been used for test purposes of this article
I've prepared a simple class that has the following functionality:
  • Start a browser's instance
  • Open a page and return page's title
  • Quit from the browser
Start a browser's instance is:

def start_browser(self, broswer_type='Firefox'):
    if re.match("firefox", broswer_type, re.I):
        self.driver = webdriver.Firefox()
    elif re.match("chrome", broswer_type, re.I):
        self.driver = webdriver.Chrome()
    else:
        self.driver = webdriver.Firefox()

We will support only two browsers Mozilla Firefox and Google Chrome with default browser Mozilla Firefox

Open a page and return page's title is:

def open_web_page(self, url):
    self.driver.get(url)
    return self.driver.title

An exit from the browser is covered by two separate functions. Selenium allows a user to open several windows in a browser. Due to this capability, Selenium has the quit() function that closes all windows in a browser and stops Selenium driver and the close() function that closes the current window of the browser, if the current window is the last window than the close() function works the same way as the quit() function. So, in our scenario, quit from the browser is:

def close_browser_window(self):
    self.driver.close()
    # Additional sleep for 5 seconds to make sure
    # that browser window has been closed properly.
    # If we remove that timeout, then call of function start_browser
    # after close_browser_window may do nothing, since instance exists
    # but closing
    sleep(3)

def stop_browser(self):
    # we should check if any instance of driver exists in the system
    if self.number_of_opened_windows() != 0:
        self.driver.quit()

Frameworks for writing and launching tests

Since a test may return both pass and failure results, each examples of the test framework usage will contain two types of scenarios:
  • Scenario(s) that returns pass result
  • Scenario(s) that returns failure

Simple tests with unittest

This my article provides enough information to get down to unittest module. Here I prefer to concentrate mainly on the key points of implementation:
Setup and teardown functions on class level are required to create a driver and start the browser instance, close all open windows and quit from browser respectively:

@classmethod
def setUpClass(cls):
    print "Unittest set-up class method"
    cls.page.start_browser()

@classmethod
def tearDownClass(cls):
    print "Unittest tear-down class method"
    cls.page.stop_browser()

Setup and teardown functions on method level are required to start the browser instance if it was closed due to the quit() function with the latest opened window of the browser and close current browser window respectively:

def setUp(self):
    print "Unittest set-up method"
    if self.page.number_of_opened_windows() == 0:
        print "Start new browser instance"
        self.page.start_browser()

def tearDown(self):
    print "Unittest tear-down method"
    self.page.close_browser_window()

Both correct and incorrect tests:

def testCorrectCheckTitle(self):
    actual_title = self.page.open_web_page(yandex_url)
    self.assertEqual(yandex_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(yandex_title, actual_title))

def testIncorrectCheckTitle(self):
    actual_title = self.page.open_web_page(google_url)
    self.assertEqual(yandex_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(yandex_title, actual_title))

Part of the output with failure and results:

======================================================================
FAIL: testIncorrectCheckTitle (__main__.UnitTestExample)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training\unittest_example.py", line 39, in testIncorrectCheckTitle
self.assertEqual(yandex_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(yandex_title, actual_title))
AssertionError: Title is incorrect. Expected 'Yandex', Actual 'Google'
----------------------------------------------------------------------
Ran 2 tests in 43.165s
FAILED (failures=1)

I've highlighted the following information:
  • Name of the failed test
  • Total number of executed tests
  • Amount of failed tests
  • Assert message(s) for failed tests
Source code of the example is available here. Output log of the example is available here

Simple tests with nosetests

First of all, the documentation that is required to work with nose is available here. Pay your attention to this paragraph to understand the types of tests that could be launched by default within the nosetests framework: especially in this line: Any python source file, directory or package that matches the testMatch regular expression (by default: (?:^|[b_.-])[Tt]est) will be collected as a test (or source for collection of tests).
We can launch the unittest case and compare the results. A certain part of the log was removed so as to save space – please find the full log here. Please note that the reviewed case was launched from command line interface with activated virtual environment

(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>nosetests unittest_example.py
.F
======================================================================
FAIL: testIncorrectCheckTitle (mera.selenium_training.unittest_example.UnitTestExample)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training\unittest_example.py", line 39, in testIncorrectCheckTitle
self.assertEqual(yandex_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(yandex_title, actual_title))
AssertionError: Title is incorrect. Expected 'Yandex', Actual 'Google'
-------------------- >> begin captured stdout << ---------------------
Unittest set-up method
Start new browser instance
--------------------- >> end captured stdout << ----------------------
-------------------- >> begin captured logging << --------------------
Several lines were removed to save space …
--------------------- >> end captured logging << ---------------------
----------------------------------------------------------------------
Ran 2 tests in 44.355s
FAILED (failures=1)
(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>

We may notice that the format of the output differs from the unitest output format. The value of verbosity parameter by default equals to 1. It is the reason why we may notice .F at the beginning of the log. As we know from unittest framework . (dot) means that the test has been completed successfully and F (capital f) means failed result.
Lets talk about the nosetests itself. What kind of benefits we may achieve using this framework? The main advantage of nosetests usage is an extended version of fixtures. The nosetests framework has the following fixtures (depends on fixture's level):
  • Test package: Nosetests provides the capability of grouping tests in packages. So, it allows package level setup and teardown function that should be declared in __init__.py module of the package. List of names for setup and teardown functions on package level is:
    • Setup functions: setup, setup_package, setUp, setUpPackage
    • Teardown functions: teardown, teardown_package, tearDown, tearDownPackage
  • Test module: It is a module with name that satisfy to testMatch regular expression. List of names for setup and teardown functions on module level is:
    • Setup functions: setup, setup_module, setUp, setUpModule
    • Teardown functions: teardown, teardown_module, tearDown, tearDownModule
  • Test classes: it could be a class in a module that matches testMatch regular expression or is a subclass of unittest.TestCase. At the level of classes both setup and teardown fixtures could have following names:
    • Setup functions: setup_class, setupClass, setUpClass, setupAll, setUpAll
    • Teardown functions: teardown_class, teardownClass, tearDownClass, teardownAll, tearDownAll
    • Like unittest.TestCase subclasses, other test classes can define setUp and tearDown methods that will be run before and after each test method.
  • Test functions: set of functions in a test module that matches testMatch criteria. Major notice here is that test functions may define setup and teardown attributes, which will be run before and after the test function, respectively. For that purpose special decorator with_setup exists. This decorator is useful only for test functions and should not be used for test methods in unittest.TestCase subclasses or other test classes
Unittests module has a big list of assert functions. We may use it if our class is subclass of the unittest.TestCase class. But nosetests allows us to use the class that is not a subclass of unittest.TestCase class.
In this case we should use nose testing tools that are described here


Lets look at the simple implementation of tests according to the information above. As part of our tutorial we will look at test classes and test functions:
Our class has following structure:


class TestNoseTestExample():
    page = HomePage()

    # This method will be called before execution of tests from the class
    @classmethod
    def setup_class(cls):
        print "NoseTests set-up class method"
        cls.page.start_browser()

    # This method will be called before execution of each test
    def setUp(self):
        print "NoseTests set-up method"
        if self.page.number_of_opened_windows() == 0:
            print "Start new browser instance"
            self.page.start_browser()

    # This method will be called after execution of each test
    def tearDown(self):
        print "NoseTests tear-down method"
        self.page.close_browser_window()
    
    # This method will be called after execution of tests from the class
    @classmethod
    def teardown_class(cls):
        print "NoseTests tear-down class method"
        cls.page.stop_browser()

    def testCorrectCheckTitleYandex(self):
        expected_title = yandex_title
        actual_title = self.page.open_web_page(yandex_url)
        eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

    def testIncorrectCheckTitleYandex(self):
        expected_title = google_title
        actual_title = self.page.open_web_page(yandex_url)
        eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

    def testCorrectCheckTitleGoogle(self):
        expected_title = google_title
        actual_title = self.page.open_web_page(google_url)
        eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

    def testIncorrectCheckTitleGoogle(self):
        expected_title = yandex_title
        actual_title = self.page.open_web_page(google_url)
        eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

The differences between this implementation and the implementation using unittest.TestCase framework:
  • Our class is not a subclass of unittest.TestCase. The nosetests supports such capability.
  • Both setup_class and teardown_class methods is used to support setup and teardown operations on the class level
  • Special functions eq_ from nosetest framework has the same role like assert of equal statement
The output has the same format as in the previous case. The only difference related to the name of the test method that has been caused:


(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>nosetests nosetest_example_class.py
..FF
======================================================================
FAIL: mera.selenium_training.nosetest_example_class.TestNoseTestExample.testIncorrectCheckTitleGoogle
----------------------------------------------------------------------
Traceback (most recent call last):
Several lines were removed to save space …
----------------------------------------------------------------------
Ran 4 tests in 129.587s
FAILED (failures=2)
(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>

Let's demonstrate the usage of decorator @with_setup with functions. This capability is useful when we would like to add some flexibility to our tests. For example, we have several groups of functions and each group has its own setup and teardown. It can be solved by including each group in a separate class. But for list of functions we will achieve the same goal using the @with_setup decorator. For example, in our case let's set the site's URL in set-up level, not inside the test. I have two different URLs, so my setup and teardown methods have the following format:

# This method will be called before execution of special tests
def setup_yandex():
    global url, page
    print "NoseTests set-up method for Yandex"
    url = yandex_url
    if page.number_of_opened_windows() == 0:
        print "Start new browser instance"
        page.start_browser()

# This method will be called before execution of special tests
def setup_google():
    global url, page
    print "NoseTests set-up method for Google"
    url = google_url
    if page.number_of_opened_windows() == 0:
        print "Start new browser instance"
        page.start_browser()

# This method will be called after execution of each test
def teardown_any():
    global page
    print "NoseTests tear-down method"
    page.close_browser_window()

Tests has the following format (on example with one test):


@with_setup(setup=setup_google, teardown=teardown_any)
def testCorrectCheckTitleGoogle():
    global url, page
    expected_title = google_title
    actual_title = page.open_web_page(url)
    eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

Execution workflow starts with setup_module function, after that (for the test above) the setup_google function will be executed, after test execution the teardown_any function will be executed. The teardown_module function will be executed in the end, when the execution of the whole list of tests is finished.
The source code for the case with classes is available here, with functions – here.
The console output from the case with classes is available here, with functions – here.
The additional feature of nosetests is test generators support that described here. Pay your attention to the following point in the documentation:
  • By default, the test name output for a generated test in verbose mode will be the name of the generator function or method, followed by the args passed to the yielded callable.
  • If you want to show a different test name, set the description attribute of the yielded callable.
  • Setup and teardown functions may be used with test generators. However, please note that setup and teardown attributes attached to the generator function will execute only once. To execute fixtures for each yielded test, attach the setup and teardown attributes to the function that is yielded, or yield a callable object instance with setup and teardown attributes.
  • For generator methods, the setUp and tearDown methods of the class (if any) will be run before and after each generated test case. The setUp and tearDown methods do not run before the generator method itself, as this would cause setUp to run twice before the first test without an intervening tearDown.
  • Please note that method generators are not supported in unittest.TestCase subclasses.
Lets look on following example:


# This method will be called before execution of all tests
def setup_module():
    print "NoseTests set-up module method for generators"
    page.start_browser()

# This method will be called before execution of special tests
def setup_any():
    global page
    print "NoseTests set-up method for generators"
    if page.number_of_opened_windows() == 0:
        print "Start new browser instance"
        page.start_browser()

# This method will be called after execution of each test
def teardown_any():
    global page
    print "NoseTests tear-down method for generators"
    page.close_browser_window()

# This method will be called after execution of tests from the class
def teardown_module():
    global page
    print "NoseTests tear-down module method for generators"
    page.stop_browser()

@with_setup(setup=setup_any, teardown=teardown_any)
def testCheckTitleFunction():
    for test_url, test_title in [[t_url, t_title] for t_url in [yandex_url, google_url] for t_title in [yandex_title, google_title]]:
    yield check_title, test_url, test_title

# Function that will be used in generator
def check_title(url, expected_title):
    global page
    actual_title = page.open_web_page(url)
    eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

# Class that will be used in generator (as callable object)
class CheckTitle():
    def __call__(self, url, expected_title):
        self.description = "Checking url: '{}' with title '{}'".format(url, expected_title)
        actual_title = page.open_web_page(url)
        eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

@with_setup(setup=setup_any, teardown=teardown_any)
def testCheckTitleClass():
    for test_url, test_title in [[t_url, t_title] for t_url in [yandex_url, google_url] for t_title in [yandex_title, google_title]]:
    yield CheckTitle(), test_url, test_title


This case demonstrates the first three points from the description of the genarators above. This information is highlighted below. Please note that according to our implementation and the rule from the list above, first four tests (.FF.) uses an instance of the browser and second four tests (.FF.) uses another one

(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>nosetests nosetest_example_generators.py
.FF..FF.
======================================================================
FAIL: mera.selenium_training.nosetest_example_generators.testCheckTitleFunction('https://www.yandex.com/', 'Google')
Several lines were removed to save space …
======================================================================
FAIL: mera.selenium_training.nosetest_example_generators.testCheckTitleFunction('http://www.google.com', 'Yandex')
Several lines were removed to save space …
======================================================================
FAIL: Checking url: 'https://www.yandex.com/' with title 'Google'
Several lines were removed to save space …
======================================================================
FAIL: Checking url: 'http://www.google.com' with title 'Yandex'
Several lines were removed to save space …
Ran 8 tests in 60.867s
FAILED (failures=4)

(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>

Source code of the case with generators is located here.
Output from the execution of this code is located here.

My latest comment about nosetests is about the execution of the tests from Python scripts. We should use the following construction to perform the execution of all tests in a module:

if __name__ == "__main__":
    nose.runmodule()

Simple tests with py.test

The py.test is the framework with huge capabilities in terms of writing tests and test execution. I recommend that you review this framework in the on-linedocumentation but concentrate mainly on the following:
  • Using py.test with functions and classes
  • Parametrized test functions
  • Marking test functions with attributes
  • Skip and xfail: dealing with tests that can not succeed
  • Continuously re-run failing tests
What is the correlation between py.test, nosetests and unittest? It is an standard question and you may find the answer (also given in the passage below) in py.test FAQ as well other useful information:
How does pytest relate to nose and unittest?
pytest and nose share basic philosophy when it comes to running and writing Python tests. In fact, you can run many tests written for nose with pytest. nose was originally created as a clone of pytest when pytest was in the 0.8 release cycle. Note that starting with pytest-2.0 support for running unittest test suites is majorly improved.

py.test framework, as well as nosetests framework, has its own test discoveryprocess. Lets repeat it here:
  • collection starts from the initial command line arguments which may be directories, filenames or test ids.
  • recurse into directories, unless they match norecursedirs pattern
  • test_*.py or *_test.py files, imported by their test package name
  • Test prefixed test classes (without an __init__ method). Source code is here and output is here
  • test_ prefixed test functions or methods are test items. Source code is here and output is here
The py.test framework provides flexible mechanism of fixtures. The purpose is to provide a fixed base line upon which tests can be reliably and repeatedly executed. More information is available here


Highlights for the py.test framework:
  • The py.test framework supports launching of tests that were developed with nosetest framework. There are several limitations to this case – you can find the details here. If we launch our case with nosetests functionality with functions, than output will be like that:(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>py.test.exe -s -v nosetest_example_functions.py
    ============================= test session starts =============================
    platform win32 -- Python 2.7.8 -- py-1.4.30 -- pytest-2.7.2 – c:\users\mikhail\documents\development\virtual_environments\selenium_training\scripts\python.exe
    rootdir: C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training, inifile:
    collected 4 items
    nosetest_example_functions.py::testCorrectCheckTitleYandex NoseTests set-up module method
    NoseTests set-up method for Yandex
    PASSED
    NoseTests tear-down method
    nosetest_example_functions.py::testIncorrectCheckTitleYandex NoseTests set-up method for Yandex
    Start new browser instance
    FAILED
    NoseTests tear-down method
    … Several lines has removed to save space … ================================== FAILURES ===================================
    ________________________
    testIncorrectCheckTitleYandex ________________________
    @with_setup(setup=setup_yandex, teardown=teardown_any)
    def testIncorrectCheckTitleYandex():
    global url, page
    expected_title = google_title
    actual_title = page.open_web_page(url)
    > eq_(expected_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(expected_title, actual_title))

    nosetest_example_functions.py:68:
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    a = 'Google', b = 'Yandex'
    msg = "Title is incorrect. Expected 'Google', Actual 'Yandex'"
    def eq_(a, b, msg=None):
    """Shorthand for 'assert a == b, "%r != %r" % (a, b)
    """
    if not a == b:
    > raise AssertionError(msg or "%r != %r" % (a, b))
    E AssertionError: Title is incorrect. Expected 'Google', Actual 'Yandex'
    ..\..\..\virtual_environments\selenium_training\lib\site-packages\nose\tools\trivial.py:29: AssertionError

    … Several lines has removed to save space …
    ==================== 2 failed, 2 passed in 124.12 seconds =====================(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>

    Full output is available here
  • The py.test framework supports launching of tests that was developed using unittest.TestCase functionality. More information here. If we launch our unittest.TestCase example, than the output will look like that:(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>py.test.exe -v -s unittest_example.py
    ============================= test session starts =============================
    platform win32 -- Python 2.7.8 -- py-1.4.30 -- pytest-2.7.2 – c:\users\mikhail\documents\development\virtual_environments\selenium_training\scripts\python.exe
    rootdir: C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training, inifile:
    collected 2 items
    unittest_example.py::UnitTestExample::testCorrectCheckTitle Unittest set-up class method
    Unittest set-up method
    Unittest tear-down method
    PASSED
    unittest_example.py::UnitTestExample::testIncorrectCheckTitle Unittest set-up method
    Start new browser instance
    Unittest tear-down method
    FAILED
    Unittest tear-down class method

    ================================== FAILURES ===================================
    ___________________ UnitTestExample.testIncorrectCheckTitle ___________________

    self = <mera.selenium_training.unittest_example.UnitTestExample testMethod=testIncorrectCheckTitle>
    def testIncorrectCheckTitle(self):
    actual_title = self.page.open_web_page(google_url)
    > self.assertEqual(yandex_title, actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(yandex_title, actual_title))
    E AssertionError: Title is incorrect. Expected 'Yandex', Actual 'Google'
    unittest_example.py:39: AssertionError

    =====================
    1 failed, 1 passed in 41.72 seconds =====================(selenium_training) C:\Users\Mikhail\Documents\Development\sandbox_python\mera\selenium_training>
    Full output is available here.
  • Several facts about assertion in tests:
    • Definition of the assert statement is: assert expression or assert expression custom_message
    • If the custom_message provided, then no assertion introspection takes places at all and the custom_message will be shown in the traceback.
    • Raising of exception can be checked with pytest.raises expression. More information about usage is here
    • The py.test framework provides smart comparison for a number of cases. Detailed information is here
    • User custom assertion could be add by implementation of the pytest_assertrepr_compare hook. As well as in previous cases, more details here
  • Fixtures. As I mentioned above, fixtures in the py.test increase flexibility of tests' configuration significantly. In the documentation we may notice the following:
    • fixtures have explicit names and are activated by declaring their use form test functions, modules, classes or whole projects.
    • Fixtures are implemented in a modular manner, as each fixture name triggers a fixture function which can itself be fixtures.
    • Fixture management scales from simple unit to complex functional testing, allowing to parametrize fixtures and tests according to configuration and component options, or to re-use fixtures across class, module or whole test session scopes.
The number of fixture's capabilities is huge, I would like to concantrate only on top of the iceberg.
  • Declaration of fixture can be done using a special decorator: @pytest.fixture
  • Fixtures can be used in different scopes of a source code or a project. You should define the scope using the parameter with the name scope, default value is function. In my examples I'm using the scope module too. You may read about it here
  • Fixtures are executed before the test method (like setup function). We could add a special finalizer method in fixture definition to make sure that the functionality of this method will be executed in the end (like teardown function):
     
    # This method will be called before execution of all tests@pytest.fixture(scope="module")def setup_module(request):
        print "Py.test set-up module method"
        page.start_browser()
        # This method will be called after execution of all tests      def fin(): 
            global
    page
            print "Py.test tear-down module method"
           page.stop_browser()
     
       request.addfinalizer(fin)
    Fixtures without the finalizer method will not execute anything after test execution.
    This is described here
  • Fixtures can be parametrized. This capability is described here. It allows to cover a large number of configurations with a small number of tests. Please look at my example here. This process of grouping fixtures is described here.
    • The py.test supports the parameters processing that needs to be skipped or produce the expected fail results – please read the description in official documentation. I've added my example that demonstrates the usage of xfail capability here. Output from the launch is here. This functionality is also supported in unittest.TestCase (Python 2.X or Python 3.X)
  • The py.test supports both capabilities: parametrizing of fixtures and parametrizing of functions that is described here. You can find the the example and the output by following the links for fixtures source/output and functions source/output.
    I would like to provide several snippets of both source code with fixtures and source code with functions here:
    yandex_title = "Yandex"yandex_url = "https://www.yandex.com/"google_title = "Google"google_url = "http://www.google.com"
    For functions:
    @pytest.mark.parametrize("test_url,test_title",     [(yandex_url, yandex_title),
         (yandex_url, google_title),
         (google_url, google_title),
         pytest.mark.xfail((google_url, yandex_title))])@pytest.mark.usefixtures("setup_module", "setup_function")def test_CheckTitle(test_url,test_title):
        global page
        actual_title = page.open_web_page(test_url)
        assert test_title == actual_title, "Title is incorrect. Expected '{}', Actual '{}'".format(test_title, actual_title)
    Fixtures. The additional function
    id_fixture_function and the fixture's parameter ids are used for several purposes. The first purpose is to provide more description in the output (example of the output is here), and the second purpose is to filter the list of executed tests by special substring. If the test id contains this substring then the test will be executed (example of the output is here):# This function returns id for fixture valuedef id_fixture_function(fixture_value):        return "Url: '{}'. Title: '{}'".format(*fixture_value)
    @pytest.fixture(scope="function",    params = [[t_url, t_title] for t_url in [yandex_url, google_url] for t_title in [yandex_title, google_title]],
        ids = id_fixture_function)
    def setup(request):
        global url, title, page
        print "Py.test set-up method"    url = request.param[0]
        title = request.param[
    1]
        if page.number_of_opened_windows() == 0:
            print "Start new browser instance"       
            page.start_browser()
           # This method will be called after execution of each test           def fin():
                global page
                print "Py.test tear-down method"            page.close_browser_window()
            request.addfinalizer(fin)


If we need to execute the py.test inside Python script with tests, we should keep in mind that script's name must have one of the following formats test_*.py or *_test.py. Command below should be used for launching the py.test framework with this script. Parameters of the main function is a list of command line arguments that will be used if tests from the module are launched in command line:
if __name__ == "__main__":
    pytest.main(args=['-v', '-s'])