Pytest fixtures on human

Introduction

hello community,

I would like to share with you my explanation of how to understand fixtures and how to start using them in your projects, thereby starting to enjoy life)

Probably, even advanced QA Automation will find something new, but my goal is to explain this topic to beginners on the fingers, because it is in it that gags often occur.

conftest.py file

This is a special pytest file that he looks into even before he runs the tests. Therefore, it is mainly used to create fixtures within our project.

This file is usually located in the root of the project, let’s take the basic PageObject structure as an example:

Basic PageObject
Basic PageObject

What is a fixture

Fixture is a function turned into a decorator, which in 99% of cases is designed to generate some data before the test and return it for use in the test or before the test, for example:

  • Create a connection to the database before the test and disconnect from it after the test is completed

  • Initialize the browser driver and close the session after the test is completed

  • Log in before starting the test and do not waste time logging in

  • Create a new account before the test, use it in the test and delete it at the end of the test, etc.

Using fixtures via return and passing it to the test as an argument

Let’s start with a simple example:

def connect_to_database():
    connection = "Соединение с базой данных установлено"
    return connection

As you can see, we have the simplest function, it figuratively returns a connection to the database.

What if we want to use it as a wrapper for the test, i.e. connect to the database before the test, and use this data in the test?

The idea comes to mind to import this function and call it in each test, or more complicated, to make a decorator out of this function, a wrapper for tests (just what we need).

This is where fixing will help us, i.e. turning our function into a fixture (in fact, this is what the decorator is called, but in the context of pytest)

In order to register a function as a fixture, pytest has a special marker @pytest.fixtureit must be written above the desired function.

Let me remind you everything are common we write fixtures in a file conftest.pythey will be visible to all test classes by default.

import pytest


@pytest.fixture # Таким образом функция будет восприниматься, как фикстура
def connect_to_database():
    connection = "Соединение с базой данных установлено"
    return connection

conftest.py

But how to use it? There are several ways, we will start with the simplest, namely we will pass the fixture as an argument inside our test / test method.

import pytest


@pytest.fixture # Таким образом функция будет восприниматься, как фикстура
def connect_to_database():
    connection = "Соединение с базой данных установлено"
    return connection

conftest.py

import pytest


class TestExample:

    def test_1(self, connect_to_database): # Прокидываем фикстуру в тест и она выполнится перед тестом
          print(connect_to_database) # Используем фикстуру, она вернет нам текст с подключением к БД

test_example.py

As a result, before starting the test, we will see the same message in the console “Database connection established”which would mean doing a db connection or any other code before the test.

As you can see, there are no imports, the test files automatically find the fixture from conftest.py without any imports, just pass the fixture as an argument to the test method.

But what if the fixture returns several objects, for example, a generated username and password, then how to use them in the test?

import pytest
import time


@pytest.fixture
def generate_data():
    login = f"autotest_{time.time()}@hyper.org" # Генерирует логин
    password = "512" # Назначает пароль
    return login, password # Возвращает логин и пароль

conftest.py

Based on how it works pythonwe know that in this case it will return a tuple of data:

("autotest_12399412@hyper.org", "512") # логин, пароль

Accordingly, in order to pass data to the test, you must also pass the fixture as an argument and access the elements of the tuple by index:

import pytest


class TestExample:
  
	def test_1(self, generate_data): # Прокидываем фикстуру в тест и она выполнится перед тестом
        login = generate_data[0] # Записываем в переменную сгенерированный логин
        password = generate_data[1] # Записываем в переменную полученный логин
        ...

test_example.py

All this is cool, but not very convenient, because in order to find out what we are referring to, we need to go to the fixture itself, since from the line generate_data[0] nothing will be clear.

The output is as follows, you can return values ​​as a dictionary and access elements by key:

import pytest
import time


@pytest.fixture
def generate_data():
    login = f"autotest_{time.time()}@hyper.org" # Генерирует логин
    password = "512" # Назначает пароль
    return {"login":login, "password": password} # Возвращает логин и пароль

conftest.py

import pytest


class TestExample:

    def test_1(self, generate_data): # Прокидываем фикстуру в тест и она выполнится перед тестом
        login = generate_data["login"] # Записываем в переменную сгенерированный логин
        password = generate_data["password"] # Записываем в переменную полученный логин
        ...

test_example.py

Now it’s much clearer.)

Using fixtures via request.cls and calling it with a marker

The next way to use fixtures is request.cls

I will try to explain in the simplest possible language, for this we will take the same example with generating a login and password:

  1. As an argument inside our fixture we drop request

  2. Assign a variable with request.cls.variable_name

import pytest
import time


@pytest.fixture
def generate_data(request):
    request.cls.login = f"autotest_{time.time()}@hyper.org" # Генерирует логин
    request.cls.password = "512" # Назначает пароль

conftest.py

The bottom line is that when you declare a variable via request.cls.variable_name, such an attribute will be automatically created in the test class. This is the same as if you directly declared them in init test class:

import pytest


class TestExample:
    # Данный кусок кода = requst.cls.login и request.cls.password в фикстуре
	def __init__(self):
        self.login = f"autotest_{time.time()}@hyper.org"
        self.password = "512"
  • request – create

  • cls – inside the class

  • variable_name – class attribute

создать.внутри_класса.имя_переменной = request.cls.login

In the case of using request.cls, the return method does not need to be written, but it must always be passed to the request fixture.

Now let’s move on to the usage stage. If you use the method described above, then you do not need to pass the fixture into the test as an argument, everything is more interesting here, we will use a special pytest marker:

The marker is written either above the desired test, in case you want to generate data from the fixture for only one test, or above the class, if you want to generate new data for each test.

import pytest
import time


@pytest.fixture
def generate_data(request):
    request.cls.login = f"autotest_{time.time()}@hyper.org" # Генерирует логин
    request.cls.password = "512" # Назначает пароль

conftest.py

import pytest

@pytest.mark.usefixtures("generate_data") # Вызываем фикстуру над классом
class TestExample:

    def test_1(self):
        print(self.login) # Сюда передастся логин
        print(self.password) # А сюда пароль

test_example.py

If you called the fixture via a marker and used request.cls to create data in the fixture, then the username and password will be available via the self parameter directly!

I hope you figured it out) Now, by analogy, let’s make a fixture to initialize the driver:

import pytest
import time

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service


@pytest.fixture
def get_driver(request):
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service)
    request.cls.driver = driver

conftest.py

Now, having called the fixture directly in the test using the marker, you can safely access the driver and its methods via self.driver.

import pytest


@pytest.mark.usefixtures("get_driver") # Вызываем фикстуру над тестом
class TestExample:
  
	def test_1(self):
	    self.driver.get("https://yandex.ru") # Работаем с полученным обьектом драйвера

test_example.py

Automatic use of fixtures

Everything is simple here, the fixture has a parameter:

In case you put autouse=Truethe fixture will be called for absolutely every test in the project automatically, without explicitly calling it anywhere.

import pytest
import time

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service


@pytest.fixture(autouse=True) # Пока не обращаем внимания на эту строку
def get_driver(request):
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service)
    request.cls.driver = driver

conftest.py

import pytest


class TestExample:

    def test_1(self):
        self.driver.get("https://vk.com") # И никаких явных вызовов фикстуры

test_example.py

Accordingly, to avoid problems and conflicts between different fixtures, I recommend using this attribute exclusively for driver initialization.

Pre- and post-conditions in fixtures

In order for the fixture to do something before the test, for example, initializing the driver, and after the test, for example, closing the browser, there is a special function:

  • yield – this is a separator, everything that is written above it will be executed before the test, everything below it will be executed after the test.

import pytest
import time

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service


@pytest.fixture(autouse=True)
def get_driver(request):
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service)
    request.cls.driver = driver
    yield
    driver.quit()

conftest.py

If we remember how a standard decorator is created, then it takes a function. And everything above it is preconditions, and everything after it is postconditions.

def decorator(test): # Принимает в себя функцию
    def wrapper():
        print("Перед тестом") # Предусловия
        test() # Функция которую декарируем
        print("После теста") # Постусловия
    return wrapper
  
@decorator # Применяем декоратор
def hello():
    print("Привет")

example.py

Accordingly, everything is similar, during the launch of tests, instead of yield our test is substituted.

Fixture scope

The scope determines how the fixture is applied, for the entire test suite (for a test class) once, or for each test (test method) separately.

The scope parameter is responsible for the fixture parameter:

In general, for 99% of cases, it is enough to know only 2 scopes:

  1. scope=”class” – the fixture will be called once for all tests within the test class

    • Example 1: Take a driver initialization fixture

      import pytest
      import time
      
      from selenium import webdriver
      from webdriver_manager.chrome import ChromeDriverManager
      from selenium.webdriver.chrome.service import Service
      
      
      @pytest.fixture(autouse=True, scope="class")
      def get_driver(request):
          service = Service(ChromeDriverManager().install())
          driver = webdriver.Chrome(service=service)
          request.cls.driver = driver
          yield
          driver.quit()

      conftest.py

      And for example, let’s do 2 tests

      import pytest
      class TestExample:
      	def test_1(self):
          self.driver.get("<https://vk.com>")
      	
      	def test_2(self):
          self.driver.get("<https://ya.ru>")

      test_example.py

      Then the browser will open, and all tests will pass in the same window. The browser will close only when all the tests have passed, and this, at least, will not allow us to run the tests in parallel, because they will interrupt each other.

    • Example 2: Login generation

      import pytest
      import time
      
      
      @pytest.fixture(scope="class")
      def generate_login(request):
          request.cls.login = f"autotest_{time.time()@hyperr.org" # Генерирует логин

      conftest.py

      When using a fixture, the login will be generated once and will be the same for all tests within the class.

      import pytest
      
      
      @pytest.mark.usefixtures("generate_login") # Вызов фикстуры генерации логина
      class TestExample:
      	def test_1(self):
              print(self.login)
      	
      	def test_2(self):
              print(self.login)

      test_example.py

      As a result, we will get the same login in every test. This can help in dependent tests, but nevertheless, atomicity is the key to quality tests. Here we come to the second scope.

  2. scope=function” – the fixture will be called for each test separately, atomically.

    This means that for each test, in the case of a fixture with a driver, for example, a new browser session will be opened, and when generating data, new data will be generated for each test.

    • Example 1: Take a driver initialization fixture

      import pytest
      import time
      
      from selenium import webdriver
      from webdriver_manager.chrome import ChromeDriverManager
      from selenium.webdriver.chrome.service import Service
      
      
      @pytest.fixture(autouse=True, scope="function")
      def get_driver(request):
          service = Service(ChromeDriverManager().install())
          driver = webdriver.Chrome(service=service)
          request.cls.driver = driver
          yield
          driver.quit()

      conftest.py

      And for example, let’s do 2 tests

      import pytest
      
      
      class TestExample:
        
      	def test_1(self):
              self.driver.get("https://vk.com")
      	
      	def test_2(self):
              self.driver.get("https://ya.ru")

      test_example.py

      Then the browser will be initialized for each test separately, which will allow you to run the tests in parallel, or simply not collide with each other and literally run them atomically.

    • Example 2: Login generation

      import pytest
      import time
      
      
      @pytest.fixture(scope="function")
      def generate_login(request):
          request.cls.login = f"autotest_{time.time()}@hyper.org" # Генерирует логин
      

      conftest.py

      When using a fixture, a new login will be generated for each test

      import pytest
      
      
      @pytest.mark.usefixtures("generate_login") # Вызов фикстуры генерации логина
      class TestExample:
      	
          def test_1(self):
              print(self.login)
      	
      	def test_2(self):
              print(self.login)

      test_example.py

      As a result, completely different data in each new test.

Conclusion

I hope this article has finally helped you figure out how fixtures work and how to work with them.

In the article, I used the most human language, avoiding abstruse technical terms, because it is much more important to explain in such a way that a person understands, and not so that he, after reading half of the article, abandons it and goes looking for another one.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *