Мокинг/патчинг переменных класса и экземпляра в тестировании Python

В этой статье представлен обзор того, как использовать модуль unittest для моделирования или исправления переменных класса и экземпляра в классе Python. К концу этой статьи вы получите следующие знания:

  • Как исправить/ имитировать атрибуты класса.
  • Как исправить/имитировать атрибуты экземпляра.
  • Различие между patch и patch.object в модуле unittest.

Модуль unittest на Python представляет собой интегрированную платформу тестирования, которая предлагает ряд инструментов для написания и выполнения тестовых примеров. Он является неотъемлемой частью стандартной библиотеки Python и используется для выполнения модульного тестирования - важнейшей практики в разработке программного обеспечения, которая обеспечивает точность отдельных компонентов или функций в программе.

Каждый класс тестовых примеров обычно включает методы, которые исследуют конкретные аспекты вашего кода. Эти методы обычно называются с префиксом “test” в качестве префикса и включают утверждения для проверки ожидаемого поведения.

Использование patch():

1. Имитация/исправление атрибута класса!

Атрибуты класса - это переменные или методы, которые принадлежат классу и являются общими для всех экземпляров класса.

Теперь давайте начнем с создания макета переменной класса. Ниже приведен простой класс с именем “Car” в файле “variables.py”, который включает “cost” в качестве переменной класса или атрибута.

#variables.py file

class Car:
    cost = 2000

print(Car.cost) # prints 2000

Когда мы печатаем Car.cost, отображается ожидаемое значение 2000. Однако я намерен изменить это поведение; моя цель - отображать 5000 при выводе значения стоимость. Для достижения этой цели мы можем использовать функцию patch из модуля unittest.mock.

Функция patch предназначена для модификации объектов в целях тестирования. Она оказывается полезной для манипулирования атрибутами классов, экземпляров и даже атрибутами модулей. Его универсальность позволяет использовать его в самых разных сценариях, где требуется модификация.

Чтобы проиллюстрировать эту функциональность, мы создадим класс TestCar, посвященный тестированию этих изменений, в отдельном файле с именем test_variables.py.

# test_variables.py

from unittest.mock import patch # import patch from unittest.mock file

from variables import Car  # importing Car class from variables file


class TestCar:
    def test_value_with_patch(self):
        # mocking a class variable
        # provide `file_name.class_name.variable_name` as str
        with patch('variables.Car.cost', 5000):
            print("\n", Car.cost) # this prints 5000

Мы используем контекстный менеджер (используя инструкцию with), который использует функцию patch для временного изменения переменной класса cost.

Функция patch требует указания полного имени целевой переменной класса, которую мы собираемся изменить, а также нового значения, которое временно присваивается переменной класса. В данном конкретном случае мы указываем variables.Car.cost в качестве пути к переменной класса cost, а значение 5000 является тем, которому мы временно присваиваем cost.

В случае variables.Car.cost термин variables соответствует имени файла, Car означает имя класса, а cost представляет переменную класса, которую мы используем. временно корректируются для возврата значения 5000.

2. Мокинг/патчинг метода класса!

Теперь, когда мы узнали, как исправить переменную класса, давайте перейдем к пониманию того, как исправить метод класса.

Мы добьемся этого, написав метод класса с именем get_cost(), который возвращает значение переменной класса cost.

# variables.py

class Car:
    cost = 2000
    
    @classmethod
    def get_cost(cls):
        return cls.cost

print(Car.cost) # prints 2000
print(Car.get_cost()) # prints 2000

Строка print(Car.get_cost()), которая в данный момент выводится 2000. Я намерен изменить поведение метода класса get_cost(), издеваясь над ним, чтобы он возвращал значение 10000.

Для достижения этой цели я создам новый тестовый пример с именем test_get_cost() в классе TestCar. Этот тестовый пример будет разработан для оценки функциональности метода класса get_cost().

# test_variables.py

from unittest.mock import patch

from variables import Car  # importing Car class from variables file

class TestCar:
    def test_value_with_patch(self):
        # mocking a class variable
        # provide `file_name.class_name.attribute_name` as str
        with patch('variables.Car.cost', 5000):
            print("\n", Car.cost)
    
     def test_get_cost(self):
        # mocking a class method
        # provide `file_name.class_name.attribute_name` as str
        # return_value tells the what to return for the get_cost()
        with patch("variables.Car.get_cost", return_value=10000):
            print("\n", Car.get_cost()) # this prints 10000
            # still prints 2000, because we are patching get_cost not cost
            print(Car.cost) # prints 2000 still

Обратите внимание на строку with patch("variables.Car.get_cost", return_value=10000). В данном случае мы используем метод patch для моделирования поведения get_cost(). Чтобы достичь этого, нам нужно указать полный путь к методу класса.

variables.Car.get_cost: В этой строке "переменные" относятся к имени файла, "Car" относится к имени класса, а "get_cost" означает метод класса, который мы собираемся заменить возвращаемым значением 10000.

Параметр return_value определяет конкретное значение, которое имитационная функция должна выдавать в качестве выходных данных.

Пожалуйста, помните, что мы исправляем метод get_cost(). А не непосредственно переменную cost. Итак, print(Car.cost) все еще печатает 2000.

Использование patch.object():

Если значение переменной меняется от объекта к объекту, то такие переменные называются переменными экземпляра. Для каждого объекта будет создана отдельная копия переменной экземпляра.

Переменные экземпляра не являются общими для объектов. Переменная экземпляра - это переменная, которая принадлежит объекту/экземпляру класса. Он создается при создании экземпляра и уничтожается при уничтожении экземпляра.

1. Мокинг переменной экземпляра!

Давайте создадим новую переменную экземпляра insurance со значением 5000. Измените класс Car, чтобы включить переменную экземпляра insurance.

# variables.py

class Car:
    cost = 2000
    def __init__(self):
        # creating an instance variable
        self.insurance = 5000
   
    @classmethod
    def get_cost(cls):
        return cls.cost

print(Car.cost)  # prints 2000
print(Car.get_cost()) # prints 2000

# create an instance for the class Car
car = Car()
print(car.insurance) # prints 5000

Я намерен исправить переменную insurance, которая является одной из переменных нашего экземпляра, чтобы она возвращала значение 1000. Для достижения этой цели библиотека unittest.mock предоставляет функцию patch.object(), специально разработанную для исправления атрибутов экземпляра.

Теперь давайте приступим к созданию тестового метода для манипулирования переменной экземпляра. Нам нужно будет внести изменения в файл test_variables.py, как описано ниже:

# test_variables.py

from unittest.mock import patch

from variables import Car  # importing Car class from variables file


class TestCar:
    def test_value_with_patch(self):
        # mocking a class variable
        # provide `file_name.class_name.attribute_name` as str
        with patch('variables.Car.cost', 5000):
            print("\n", Car.cost)
    
     def test_get_cost(self):
        # mocking a class method
        # provide `file_name.class_name.attribute_name` as str
        with patch("variables.Car.get_cost", return_value=10000):
            print("\n", Car.get_cost()) # this prints 10000
            # still prints 2000, because we are patching get_cost not cost
            print(Car.cost) # prints 2000 still
     
     def test_insurance_variable(self):
        # mocking an instance variable
        car1 = Car()
        # "insurance" is present in the car1 instance, so provide the name
        # remember that we have to provide the name of variable in str
        with patch.object(car1, "insurance", new=1000):
            print(car1.insurance) # prints 1000 

Следите за линией with patch.object(car1, "insurance", new=1000)

В данном контексте:

  • car1 является экземпляром класса, скорее всего, экземпляром класса Car в вашем коде.
  • "insurance" является атрибутом экземпляра car1, который вы хотите временно изменить.
  • new=1000 указывает, что вы хотите установить значение атрибута "insurance" равным 1000 на время действия блока, управляемого контекстом.

Внутри блока, управляемого контекстом, любой код, который обращается к атрибуту insurance экземпляра car1, увидит его значение как 1000 вместо исходного значения.

2. Имитация/патчинг метода экземпляра!

Теперь, когда мы узнали, как исправить переменную экземпляра, давайте перейдем к пониманию того, как исправить метод экземпляра — по сути, метод объекта.

Мы добьемся этого, добавив метод экземпляра к классу Car в файле variables.py, как показано ниже:

# variables.py

class Car:
    cost = 2000
    def __init__(self):
        # creating an instance variable
        self.insurance = 5000
   
    @classmethod
    def get_cost(cls):
        return cls.cost

    def get_insurance(self):
        return self.insurance

print(Car.cost)  # prints 2000
print(Car.get_cost()) # prints 2000

# create an instance for the class Car
car = Car()
print(car.insurance) # prints 5000

print(car.get_insurance()) # prints 5000

Я хочу поиздеваться над методом экземпляра get_insurance(), чтобы он возвращал 3000 в качестве нового значения.

Пожалуйста, помните, что я всего лишь издеваюсь над методом get_insurance(), но не над значением переменной insurance.

Давайте напишем новый тестовый пример для тестирования этого метода экземпляра get_insurance() в классе TestCar файла test_variables.py.

# test_variables.py

from unittest.mock import patch

from variables import Car  # importing Car class from variables file

class TestCar:
    def test_value_with_patch(self):
        # mocking a class variable
        # provide `file_name.class_name.attribute_name` as str
        with patch('variables.Car.cost', 5000):
            print("\n", Car.cost)
    
     def test_get_cost(self):
        # mocking a class method
        # provide `file_name.class_name.attribute_name` as str
        with patch("variables.Car.get_cost", return_value=10000):
            print("\n", Car.get_cost()) # this prints 10000
            # still prints 2000, because we are patching get_cost not cost
            print(Car.cost) # prints 2000 still
     
     def test_insurance_variable(self):
        # mocking an instance variable
        car1 = Car()
        # "insurance" is present in the car1 instance, so provide the name
        # remember that we have to provide the name of variable in str
        with patch.object(car1, "insurance", new=1000):
            print(car1.insurance) # prints 1000

    def test_insurance_method(self):
        # mocking an instance method
        car = Car()
        # "get_insurance" is present in the car instance, so provide the name
        # remember that we have to provide the name of the method in str
        with patch.object(car, "get_insurance", return_value=3000):
            print(car.get_insurance()) # prints 3000

Давайте разберем код test_insurance_method().

Контекстный менеджер patch.object из модуля unittest.mock предназначен для временного имитационного моделирования get_insurance() метода экземпляра из Car класс.

  1. patch.object(car, "get_insurance", return_value=3000): Эта строка устанавливает контекст, в котором метод get_insurance() объекта car будет временно заменен макетом, который возвращает 3000 в качестве своего результата . Параметр return_value определяет значение, которое должен возвращать mock при вызове метода.
  2. print(car.get_insurance()): Внутри контекста, когда вызывается метод get_insurance() объекта car, он возвращает имитируемое значение 3000, а не выполняет реальную логику метода. Это позволяет вам изолировать и тестировать определенные части вашего кода, не влияя на реальное поведение метода.

Цель этого кода - протестировать поведение кода, который взаимодействует с методом get_insurance() объекта car. Временно заменив метод макетом, который возвращает известное значение (в данном случае 3000), вы можете гарантировать, что результаты вашего тестирования будут предсказуемыми и согласованными, независимо от фактической реализации метода get_insurance().

Как только код внутри блока patch.object будет выполнен, исходное поведение метода get_insurance() будет восстановлено, и последующие вызовы car.get_insurance() будут выполняться как обычно.

К этому времени вы должны были понять основную разницу между patch и patch.object. Давайте еще раз подведем итог

  1. patch это более высокоуровневая функция для исправления / имитирования атрибутов класса.
  2. patch.object полезен при исправлении/издевательстве над атрибутами экземпляра экземпляра класса.
  3. Исправление атрибута экземпляра экземпляра принадлежит этому конкретному экземпляру и не применяется к другому экземпляру, поскольку каждый экземпляр класса получает новую копию атрибутов экземпляра.

Методы, которые я использовал как для patch, так и для patch.object, не являются исчерпывающими; существует несколько других способов их использования. Эти функции также могут служить декораторами. Для получения дополнительной информации, пожалуйста, обратитесь к документации, представленной в модуле unittest.

Back to Top