Как писать модульные тесты для методов экземпляра в Python

Если вы знакомы с объектно-ориентированным программированием, то знаете, что классы позволяют объединять связанные данные и поведение. Классы можно использовать в качестве шаблонов для создания экземпляров класса. Если класс в Python — это формочка для печенья, то каждый экземпляр — это печенье.

Данные и поведение описываются атрибутами и методами в определении класса.

Как протестировать методы экземпляра класса Python

Давайте разберем, как настроить модульные тесты для экземпляров классов. Мы напишем тесты для проверки функциональности класса Book:

class Book:
    def __init__(self,title,author,pages,price,discount):
        self.title = title
        self.author = author
        self.pages = pages
        self.price = price
        self.discount = discount
    def get_reading_time(self):
        return f"{self.pages*1.5} minutes"
    def apply_discount(self):
        discounted_price = self.price - (self.discount*self.price)
        return f"${discounted_price}"

Класс Book служит шаблоном и имеет такие атрибуты, как title, author, pages, price, discount (название, автор, страницы, цена и скидка). get_reading_time() и apply_discount() — это методы экземпляра, которые используют названные атрибуты.

Таким образом, мы можем создавать объекты книг из класса Book, каждый со своими атрибутами.

Иллюстрация отношений между классом Book и объектами книг (экземплярами класса Book)

Чтобы протестировать методы экземпляра get_reading_time() и apply_discount(), мы можем создать два экземпляра класса Book внутри тестовых методов. Для проверки корректности возвращаемых значений методов экземпляров можно использовать метод assertEqual().

from book import Book
import unittest

class TestBook(unittest.TestCase):
    def test_reading_time(self):
        book_1 = Book('Deep Work','Cal Newport',304,15,0.05)
        book_2 = Book('Grit','Angela Duckworth',447,16,0.15)
        self.assertEqual(book_1.get_reading_time(), f"{304*1.5} minutes")
        self.assertEqual(book_2.get_reading_time(), f"{447*1.5} minutes")
    def test_discount(self):
        book_1 = Book('Deep Work','Cal Newport',304,15,0.05)
        book_2 = Book('Grit','Angela Duckworth',447,16,0.15)
        self.assertEqual(book_1.apply_discount(),f"${15 - 15*0.05}")
        self.assertEqual(book_2.apply_discount(),f"${16 - 16*0.15}" )
        
if __name__=='__main__':
	unittest.main()

Как выделять и освобождать ресурсы во время модульных тестов

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

Но если вам нужно протестировать большое количество методов экземпляра, это не оптимально.

Будет удобнее, если мы определим метод, который будет инстанцировать эти объекты перед запуском каждого теста. Именно здесь на помощь приходит метод setUp().

Как работают методы setUp() и tearDown()

Методы setUp() и tearDown() обычно используются для выполнения взаимодополняющих задач по выделению и деаллокации ресурсов до и после каждого модульного теста.

Метод setUp() запускается перед каждым тестом, а метод tearDown() — после каждого теста.

Здесь мы можем использовать метод setUp() для инстанцирования объектов книги. Но порой необходимо использовать и метод tearDown(). Например, если каждый тест добавляет файлы в каталог или создает несколько объектов в памяти, вы можете захотеть освобождать каталог и удалять созданные объекты после каждого теста. Мы добавим метод tearDown(), чтобы убедиться, что он запускается после каждого теста.

Чтобы лучше разобраться, добавим поясняющие операторы print().

from book import Book
import unittest
class TestBook(unittest.TestCase):
    def setUp(self):
        print("\nRunning setUp method...")
        self.book_1 = Book('Deep Work','Cal Newport',304,15,0.05)
        self.book_2 = Book('Grit','Angela Duckworth',447,16,0.15)
    def tearDown(self):
        print("Running tearDown method...")
    def test_reading_time(self):
        print("Running test_reading_time...")
        self.assertEqual(self.book_1.get_reading_time(), f"{304*1.5} minutes")
        self.assertEqual(self.book_2.get_reading_time(), f"{447*1.5} minutes")
    def test_discount(self):
        print("Running test_discount...")
        self.assertEqual(self.book_1.apply_discount(),f"${15 - 15*0.05}")
        self.assertEqual(self.book_2.apply_discount(),f"${16 - 16*0.15}" )
if __name__=='__main__':
	unittest.main()

Теперь запустите модуль test_book. Вот вывод:

Output
Running setUp method...
Running test_discount...
Running tearDown method...
.
Running setUp method...
Running test_reading_time...
Running tearDown method...
.
----------------------------------------------------------------------
Ran 2 tests in 0.003s
OK

Как использовать методы setUpClass() и tearDownClass()

В дополнение к вышеперечисленным методам вы также можете использовать методы класса setUpClass() и tearDownClass().

В Python методы класса привязываются к классу, а не к конкретному экземпляру. Чтобы определить метод как метод класса, можно использовать декоратор @classmethod.

Так когда же нам следует использовать эти методы класса?

Инстанцирование объектов, как в приведенном выше примере, — простая задача, не требующая больших вычислительных затрат. Но иногда могут возникнуть задачи, которые слишком дороги, чтобы выполнять их перед каждым тестом: например, раскрутка резидентной базы данных.

Если все последующие тесты в тестовом классе только считывают некоторые данные из базы, мы можем использовать метод класса setUpClass() для раскрутки БД и метод класса tearDownClass() для ее уничтожения после выполнения всех тестов.

Собираем все вместе:

  • Метод класса setUpClass() выполняется перед запуском всех тестов
  • Метод класса tearDownClass() выполняется после выполнения всех тестов
  • Методы setUp() и tearDown() выполняются до и после каждого теста соответственно

Давайте добавим методы класса setUpClass() и tearDownClass() в класс TestBook.

from book import Book
import unittest
class TestBook(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print("\nsetUpClass method: Runs before all tests...")
    def setUp(self):
        print("\nRunning setUp method...")
        self.book_1 = Book('Deep Work','Cal Newport',304,15,0.05)
        self.book_2 = Book('Grit','Angela Duckworth',447,16,0.15)
    def tearDown(self):
        print("Running tearDown method...")
    def test_reading_time(self):
        print("Running test_reading_time...")
        self.assertEqual(self.book_1.get_reading_time(), f"{304*1.5} minutes")
        self.assertEqual(self.book_2.get_reading_time(), f"{447*1.5} minutes")
    def test_discount(self):
        print("Running test_discount...")
        self.assertEqual(self.book_1.apply_discount(),f"${15 - 15*0.05}")
        self.assertEqual(self.book_2.apply_discount(),f"${16 - 16*0.15}" )
    @classmethod
    def tearDownClass(cls):
    	print("\ntearDownClass method: Runs after all tests...")
        
if __name__=='__main__':
	unittest.main()

Теперь повторно запустите test_book.py.

Output
setUpClass method: Runs before all tests...
Running setUp method...
Running test_discount...
Running tearDown method...
.
Running setUp method...
Running test_reading_time...
Running tearDown method...
.
tearDownClass method: Runs after all tests...
----------------------------------------------------------------------
Ran 2 tests in 0.010s
OK

Результат показывает, что методы setUpClass() и tearDownClass() выполняются до и после всех тестов.

Заключение

Надеюсь, это руководство помогло вам научиться настраивать модульные тесты для методов экземпляра в Python.

Перевод статьи «How to Write Unit Tests for Instance Methods in Python».