End To End Tutorial For Pytest Fixtures With Examples
Himanshu Sheth
Posted On: May 18, 2020
138588 Views
21 Min Read
This article is a part of our Content Hub. For more in-depth resources, check out our content hub on Selenium pytest Tutorial.
While writing your Selenium Test automation scripts, you’d often require data that you want to share across multiple tests. This is done by using objects which are used to instantiate the particular dataset. In pytest, this can be easily done with the help of fixtures.
Consider a test scenario where MySQL queries are executed on a database. The execution time here depends on the size of the database and the operations can be highly CPU intensive depending on its size. In such cases, repetitive implementation and execution are avoided by using pytest fixtures as they feed data to the tests such as DB connections. They also help to instantiate Selenium WebDriver for browsers under test, URLs to test, etc.
In this part of the Selenium Python tutorial series, I’ll focus on pytest fixtures. I’d further explore why you should use it for End to End testing and then explore the scope of fixtures.
TABLE OF CONTENT
What Are pytest Fixtures?
pytest fixtures are functions attached to the tests which run before the test function is executed. Fixtures are a set of resources that have to be set up before and cleaned up once the Selenium test automation execution is completed.
pytest fixture function is automatically called by the pytest framework when the name of the argument and the fixture is the same.
A function is marked as fixture using the following marker:
1 |
@pytest.fixture |
Shown below is a sample pytest fixture function for this Selenium Python tutorial:
In the sample code shown above, the fixture function is fixture_func(). It is called when the test function test_fixture() is invoked for execution. The return value of the fixture function is passed as an argument to test_fixture(). Assert is raised if the value returned by fixture_func() does not match the expected output.
In this pytest Tutorial, learn how to use pytest fixtures with Selenium and how to set up your test using the pytest.fixture() decorator.
Why Use pytest Fixtures?
pytest fixtures are used in python instead of classic xUnit style setup and teardown methods where a particular section of code is executed for each test case.
There are various reasons for using pytest fixtures, the major ones are below:
- pytest fixtures are implemented in a modular manner. They are easy to use and no learning curve is involved.
- Like normal functions, fixtures also have scope and lifetime. The default scope of a pytest fixture is the function scope. Apart from the function scope, the other pytest fixture scopes are – module, class, and session.
- Fixtures with function scope improves the readability and consistency in the test code. This makes code maintenance much easier.
- Pytest fixtures function are reusable and can be used for simple unit-testing as well as testing complex scenarios.
- Fixtures leverage dependency injection, the popular object-oriented programming concept. Fixture functions act as injectors and test functions that use fixtures are consumers of the fixture objects.
In order to explain the importance of pytest fixtures, I’ll take a simple example where setup() and teardown() functions of one test (test_1) are called even when another test (test_2) is executed. For a simple test, this small overhead might not make such a huge difference in the execution. However, if the Selenium test automation scenarios contain frequent access to databases or tests being performed on multiple browsers & OS combinations, repetitive execution can cause a significant hit on the execution performance.
The test case test_2_not_using_resource_1() is executed by invoking the following command on the terminal
1 |
pytest --capture=no --verbose test_advantages.py::test_2_not_using_resource_1 |
As shown in the execution output, functions for resource 1 are unnecessarily invoked even though only test_2 is executed.
The issue mentioned above in this Selenium Python tutorial can be fixed by defining fixture function resource_1_setup() and resource_1_teardown(), akin to the xUnit style implementation. The scope of the fixture function is module [@pytest.fixture(scope=’module’)]
As seen in the execution snapshot, setup function for resource 1 is only called for Test_1 (and not for Test_2).
This is where pytest fixtures are effective as repetitive implementation & need for unnecessary execution is eliminated.
Scope Of pytest Fixtures
The scope of a fixture function indicates the number of times a fixture function is invoked. Here are the detailed description of the pytest fixtures scope in this Selenium Python tutorial:
- Function – This is the default value of the fixture scope. Fixture with function scope is executed once per session.
- Package (or Session) – A pytest fixture with scope as Session is created only once for the entire Selenium test automation session. Session scope is ideal for usage as WebDriver handles are available for the Selenium test automation session.
- Module – As the name indicates, a fixture function with scope as Module is created (or invoked) only once per module.
- Class – The fixture function is created once per class object.
The scope of pytest fixture function can be supplied with the @pytest.fixture marker
This PyTest Tutorial for beginners and professionals will help you learn how to use PyTest framework with Selenium and Python for performing Selenium automation testing.
Automated Browser Testing With Selenium & pytest Fixtures
In automated browser testing with Selenium, the web browser has to be loaded before the Selenium test automation is performed. Loading the browser before every test is not a good practice. Rather, the web browser should be loaded once before the tests have started and closed once the tests are complete. pytest fixtures are extremely useful when you are performing automated browser testing.
Common Selenium WebDriver implementation can be a part of the fixture function, particularly – initialization of the Selenium WebDriver for browser under test & cleanup of resources after the completion of tests. If you are new to Selenium WebDriver, I’ll recommend you check out what Is Selenium?
For demonstrating automated browser testing with pytest fixtures and Selenium WebDriver, I’ll consider the Selenium test automation scenarios mentioned below in this Selenium Python tutorial:
Test Case 1 (Test Browser – Chrome)
- Navigate to the URL https://lambdatest.github.io/sample-todo-app/
- Select the first two checkboxes
- Send ‘Happy Testing at LambdaTest’ to the textbox with id = sampletodotext
- Click the Add Button and verify whether the text has been added or not
Test Case 2 (Test Browser – Firefox)
- Navigate to the URL https://www.google.com
- Search for “LambdaTest”
- Click on the first test result
- Raise an Assert if the Page Title does not match the expected title
As there are two different Selenium test automation cases, we would need two pytest fixtures functions for initialization & de-initialization of resources for Chrome & Firefox browser respectively. The complete implementation is below:
Code WalkThrough
Step 1 – All the necessary modules for this Selenium Python tutorial example are imported at the beginning of the implementation.
Step 2 – Two pytest fixtures functions are created, one for each Selenium test automation case as different browsers are used for testing. The chrome_driver_init() function is decorated with the @pytest.fixture indicating that it will be used as a fixture function. The scope of the fixture is set to class.
request.cls will be set to None for a scope that is not of type class. Since the scope of the pytest fixtures function is set to class, request.cls is nothing but the test class that is using the function. For the test Test_URL_Chrome(), request.cls.driver will be the same as Test_URL_Chrome.driver which is the reference to the Chrome WebDriver instance.
The code after yield is run as a finalizer. Once the test is executed, the Selenium WebDriver is shut down using the close method of Selenium test automation.
1 2 |
yield chrome_driver.close() |
The implementation for the other fixture function i.e. driver_init() is the same, except that the browser being used is Firefox.
Step 3 – To ensure that the web driver is initialized only once, the fixtures are applied to the respective base classes i.e. BaseTest() and Basic_Chrome_Test(). The test classes would be extended from the respective base class.
Step 4 – The Fixture functions [driver_init() and chrome_driver_init()] are passed as arguments to the corresponding test functions.
Test_URL is the test class that is extended from the base class BasicTest.
Test_URL_Chrome is the test class that is extended from the base class Basic_Chrome_Test.
Both classes contain a single test. The tests locate the required web elements on the web page. Once located, appropriate Selenium methods [find_element_by_name(), find_element_by_id()] and necessary operations [i.e. click(), submit(), send_keys(), etc.] are performed on those elements. As this part of the Selenium Python tutorial focuses on pytest fixtures, we would not get into the minute details of the Selenium test automation implementation.
The following command is used for executing Selenium test automation:
1 |
pytest --capture=no --verbose <file_name.py> |
Shown below in this Selenium Python tutorial is the execution snapshot which indicates that both the tests executed successfully.
Test Automation Using Pytest and Selenium WebDriver
Parameterized pytest Fixtures
What if you need to execute the same tests on different web browsers e.g. Chrome, Firefox, Opera, etc., with separate pytest fixtures functions that instantiate the Selenium WebDriver for the required web browser. It is recommended to have a single fixture function that can be executed across different input values. This can be achieved via parameterized pytest fixtures, which I’ll show next in this Selenium Python tutorial.
Parameterized driver_init fixture that takes input as Chrome and Firefox are below:
1 |
@pytest.fixture(params=["chrome", "firefox"],scope="class") |
Declaration of params with @pytest.fixture contains a list of values (i.e. Chrome, Firefox) for each of which the fixture function will be executed. The value can be accessed using request.param function. Porting the code from a normal (i.e. non-parameterized) fixture to a parameterized fixture does not require any change in the feature implementation.
To demonstrate parameterized pytest features, I would execute the following test automation cases on Chrome and Firefox browsers:
Test Case 1 (Test Browsers – Chrome, Firefox)
- Navigate to the URL https://lambdatest.github.io/sample-todo-app/
- Select the first two checkboxes
- Send ‘Happy Testing at LambdaTest’ to the textbox with id = sampletodotext
- Click the Add Button and verify whether the text has been added or not
As the Selenium test automation needs to be executed on Chrome and Firefox browsers, we first create a parameterized fixture function that takes these as arguments. Depending on the browser being used for testing, an appropriate WebDriver instance for the browser is initiated i.e. if the param value is chrome, WebDriver for Chrome browser is initialized.
As shown below in this Selenium Python tutorial, request.param is used to read the value from the fixture function. The remaining implementation of the Fixture function remains the same as a non-parameterized fixture function.
Implementation
Code WalkThrough
As shown below in this Selenium Python tutorial, request.param is used to read the value from the pytest fixtures function. The remaining implementation of the Fixture function remains the same as a non-parameterized fixture function.
Rest of the implementation remains the same as Test Case (1) which is demonstrated in the section Automated Browser Testing using Selenium & pytest Fixtures. The only change is that we have used a parameterized fixture function to execute the test on Chrome and Firefox browsers.
Shown below is the Selenium test automation execution on the browsers under test:
As seen in the terminal snapshot, the test code test_open_url() is invoked separately for input values chrome and firefox.
Parameterized Test Functions
Along with parameterized test fixtures, pytest also provides decorators using which you can parameterize test functions. The @pytest.mark.parametrize decorator enables the parameterization of arguments for a test function. Using this decorator, you can use a data-driven approach to testing as Selenium test automation can be executed across different input combinations.
Here is how @pytest.mark.parametrize decorator can be used to pass input values:
1 2 |
@pytest.mark.parametrize("input_arg_1, input_arg_2,...,input_arg_n", [("input_val_1", "input_val_2",...,"input_val_n")]) |
As shown in the official documentation of parameterization in pytest, the expected output can also be supplied along with the input parameters.
In this pytest tutorial, learn how to use parameterization in pytest to write concise and maintainable test cases by running the same test code with multiple data sets.
Demonstration Of Parameterized Test Functions
To demonstrate parameterization in test functions, we perform Selenium test automation where separate web pages are opened for Chrome and Firefox browsers. Assert is raised if the page title does not match the expected title.
Implementation
As shown in the implementation above for this Selenium Python tutorial, two input arguments of type string (test_browser & test_url) are supplied to the @pytest.mark.parametrize decorator. The input values are separated by comma (,) and enclosed under [].
- Input combination (1) ? (“chrome”, “https://www.lambdatest.com/”)
- Input Combination (2) ? (“firefox”, “https://www.lambdatest.com/blog/”)
The test function uses the input arguments added via the decorator for performing the Selenium test automation.
The rest of the implementation is self-explanatory and we would not get into details of the same. Shown below in this Selenium Python tutorial is the execution snapshot which indicates that Selenium test automation was executed across both the input combinations.
Sharing pytest Fixtures Across Tests
There might be cases where pytest fixtures have to be shared across different tests. Sharing of pytest fixtures can be achieved by adding the pytest fixtures functions to be exposed in conftest.py. It is a good practice to keep conftest.py in the root folder from where the Selenium test automation execution is performed.
Shown below is conftest.py where parameterized fixture function driver_init() is added.
conftest.py
As driver_init() fixture function is now a part of conftest.py, the implementation of fixture function is removed from the test code and @pytest.mark.usefixtures decorator with input as fixture function is added in the test file.
1 |
@pytest.mark.usefixtures("driver_init") |
Below is the snippet of the implementation in the test file:
Rest of the implementation remains the same as the one demonstrated in the section Parameterized Fixtures. As seen from the code snippet, the fixture implementation is no longer a part of the test code as it is now shifted to conftest.py. We do not import conftest.py in the test code as the pytest framework automatically checks its presence in the root directory when compilation is performed.
Skip & Xfail Tests In pytest
There are cases where a test might not be relevant for a particular platform or browser. Rather than executing the Selenium test automation case for that platform and expecting it to fail, it would be better if the test is skipped with a valid reason.
A skip in pytest means that the test is expected to pass only on if certain conditions are met. Common cases are executing certain cross browser tests on the latest browsers such as Chrome, Firefox, etc. & skipping on Internet Explorer with a reason.
A xfail means that the test is expected to fail due to some reason. A common example is a test for a feature that is yet to be implemented. If the test marked as xfail still happens to pass, it is marked as xpass (and not pass).
xfail tests are indicated using the following marker:
1 |
@pytest.mark.xfail |
Tests can be skipped for execution using the following marker:
1 |
@pytest.mark.skip |
Tests that skip, xpass, or xfail are reported separately in the test summary. Detailed information about skipped/xfailed tests is not available by default in the summary and can be enabled using the ‘–r’ option
1 |
pytest -rxXs |
Skipping Test Functions
A test function that has to be skipped for execution can be marked using the skip decorator along with an optional reason for skipping the test.
1 2 3 |
@pytest.mark.skip(reason="reason to be skipped") def test_a_feature(): ................. |
For conditional skip, the @pytest.mark.skipif marker can be used to skip the function if a condition is True. In the example shown below for this Selenium Python tutorial, test_function() will not be executed (i.e. skipped) if the Python version is less than 3.8.
Detailed information about skip & skipif markers is available on the official documentation of pytest here & here.
Xfail – Marking test functions expected to fail
The xfail marker is used to mark a test function that is expected to fail.
1 2 3 |
@pytest.mark.xfail def test_func(): ................. |
If a test fails only under a certain condition, the test can be marked as xfail with a condition and an optional reason that is printed alongside the xfailed test.
Xfail and Skip markers can also be used along with fixtures in pytest. The respective markers can be supplied along with the parameters in a parameterized fixture. Sample code snippet is below:
To demonstrate the usage of xfail and skip markers with parameterized fixtures, we take sample test cases which are executed on Chrome, Firefox, and Safari browsers. As seen in the snippet above:
- Test on Firefox is marked as xfail
- Test on Chrome is a regular test and marked with a marker pytest.mark.basic
- Test on Safari is marked as skip hence, it will not be executed
The complete implementation is below:
The test case to be executed on Firefox is marked as xfail but the test case passes. Hence, the final status of the test on Firefox is xpass. Test on Chrome browser is marked with a marker pytest.mark.basic. It executes successfully and hence the status is pass. The final test is on Safari browser and is marked with the skip marker. Hence, it is skipped for execution. Shown below in this Selenium Python tutorial is the execution snapshot:
We use the earlier example to demonstrate usage of xfail and skip markers, with the markers applied on the individual test cases.
The test cases test_chrome_url() and test_firefox_url() are marked as xfail but they execute successfully. Hence, the result for these test cases is xpass. On the other hand, the final test test_safari_url() is marked with pytest.mark.skip marker and hence, will be skipped from execution. Shown below is the execution snapshot:
Also read: Pytest Tutorial: Executing Multiple Test Cases From Single File
This certification is for professionals looking to develop advanced, hands-on expertise in Selenium automation testing with Python and take their career to the next level.
Here’s a short glimpse of the Selenium Python 101 certification from LambdaTest:
Also explore 35 Commonly Asked Pytest Interview Questions in 2023. Perfect for interview prep or boosting your Pytest knowledge.
Wrapping It Up!
Pytest fixtures are functions that are run before each function to which it is applied is executed. Fixtures can be used for simple unit testing as well as testing for complex scenarios. Pytest fixtures are ideal for usage in cross browser testing as browser resources need not be instantiated every time when a test is executed.
Function, module, class, and session are the different scopes available with fixture functions. Pytest Fixtures, as well as test functions, can be parameterized. conftest.py is used to share fixtures across tests.
Feel free to retweet and share this article with your peers! Do let us know of any queries or doubts you might have in the comment section down below. That’s it for now! Happy Testing!!!
Frequently Asked Questions (FAQs)
What is Pytest?
Pytest is a testing framework for Python that simplifies the process of writing and running tests. It offers powerful features such as test discovery, assertions, fixtures, and plugins, making it popular among developers.
How to use Pytest?
To use Pytest, write test functions using the test_
prefix, define assertions to check expected outcomes, and run tests using the pytest
command in the terminal. Pytest automatically discovers and executes the tests.
What is a Pytest fixture?
A Pytest fixture is a reusable setup or data that can be utilized by multiple tests. It provides a convenient way to initialize resources, such as database connections or test data, before test functions are executed.
What is Pytest testing?
Pytest testing refers to the process of writing and executing tests using the Pytest framework. It allows developers to define test functions, organize tests into test modules or packages, and generate comprehensive test reports.
How do I add a fixture in Pytest?
To add a fixture in Pytest, use the @pytest.fixture decorator before a function definition. This makes the function a fixture, which can be invoked by test functions as an argument.
What is the use of fixtures in Python?
Fixtures in Python, specifically in Pytest, serve as a way to provide reusable setup or data for tests. They help in isolating test cases, managing resources, and reducing code duplication.
Why are fixtures used?
Fixtures are used to ensure the consistent setup of test environments and provide necessary dependencies or data to tests. They promote test reliability, readability, and maintainability by eliminating redundant setup code.
What is the advantage of a fixture?
The advantage of fixtures lies in their ability to simplify test setup and teardown, reduce code duplication, and enhance test maintainability. They ensure consistent and isolated test environments, improving the reliability and effectiveness of test cases.
Got Questions? Drop them on LambdaTest Community. Visit now