unit тесты python

Python: Автоматизация тестирования с Unittest

Сегодня, в условиях постоянно развивающейся разработки ПО, обеспечение надежности и стабильности кодовой базы имеет первостепенное значение. Одним из наиболее эффективных способов достижения этих целей является автоматизированное тестирование. Автоматизация тестирования позволяет систематически проверять корректность кода, выявлять баги на ранней стадии и поддерживать качество программного обеспечения на протяжении всего жизненного цикла.

Python, благодаря своей простоте и универсальности, предоставляет мощные инструменты для автоматизации тестирования. Среди них фреймворк unittest выделяется как надежное и многофункциональное решение для написания и запуска автотестов. В данной статье мы рассмотрим основы автоматизации тестирования на Python с unittest.

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

Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.

Смотрите также: “Основы автоматизации с помощью Selenium и Python”.

Предположим, у вас есть простой скрипт на Python с функцией, которая складывает два числа. Вот этот скрипт (math_functions.py):

def add(a, b):
    return a + b

Теперь давайте напишем тест-кейсы для этого скрипта, используя unittest. Мы создадим класс теста, который наследуется от unittest.TestCase и определим в нем методы тестов.

  1. Импортируем unittest и функцию add из нашего скрипта (math_functions.py).
  2. Создаем класс с именем TestMathFunctions который наследуется от unittest.TestCase. Каждый тест-кейс – это метод в внутри класса.
  3. Убеждаемся, что название каждого теста начинается с test_, что является соглашением об именовании, признанным unittest для идентификации методов тестирования.
  4. Внутри каждого метода тестирования мы используем ассерты по типу  assertEqual для проверки ожидаемого поведения нашей функции add.
  5. Наконец, мы используем unittest.main() для запуска тестов при выполнении скрипта.
import unittest
from math_functions import add

class TestMathFunctions(unittest.TestCase):

    def test_add_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)

    def test_add_negative_numbers(self):
        self.assertEqual(add(-2, -3), -5)

    def test_add_mixed_numbers(self):
        self.assertEqual(add(2, -3), -1)
        self.assertEqual(add(-2, 3), 1)

if __name__ == '__main__':
    unittest.main()

Вы должны увидеть результат, прошли тесты или упали. Если все тесты будут пройдены, выведется сообщение по типу:

unittest тесты пройдены

Если какой-либо тест завершился неудачей, на выходе будет представлена подробная информация, какой тест (тесты) провалился и почему.

unittest тесты failed

Часто используемые ассерты в unittest

1. assertEqual(a, b): проверяет, равны ли a и b.

self.assertEqual(add(2, 3), 5)

2. assertNotEqual(a, b): проверяет, не равны ли a и b.

self.assertNotEqual(add(2, 3), 6)

3. assertTrue(x): проверяет, что x равно True.

self.assertTrue(result)

4. assertFalse(x): проверяет, что x равно False.

self.assertFalse(error_occurred)

5. assertIs(a, b): проверяет, что a идентично (проверяет, ссылаются ли a и b на один и тот же объект).

self.assertIs(result, expected_result)

6. assertIsNot(a, b): проверяет, что a не идентично b (проверяет, не ссылаются ли a и b на один и тот же объект).

self.assertIsNot(result, expected_result)

7. assertIsNone(x): проверяет, что x равно None.

self.assertIsNone(error)

8. assertIsNotNone(x): проверяет, что x не равно None.

self.assertIsNotNone(result)

9. assertIn(a, b): проверяет, что a содержится в b.

self.assertIn('apple', fruits)

10. assertNotIn(a, b): проверяет, что a не содержится в b.

self.assertNotIn('banana', fruits)

11. assertGreater(a, b): проверяет, что a больше чем b.

self.assertGreater(result, 0)

12. assertGreaterEqual(a, b): проверяет, что a больше или равно b.

self.assertGreaterEqual(result, 0)

13. assertLess(a, b): проверяет, что a меньше чем b.

self.assertLess(result, 100)

14. assertLessEqual(a, b): проверяет, что a меньше или равно b.

self.assertLessEqual(result, 100)

15. assertRaises(exception, callable, *args, kwargs): проверяет, вызывает ли функция callable с аргументами args и kwargs исключение указанного типа exception.

with self.assertRaises(ValueError):
    divide(10, 0)

Предположим, у нас есть функция divide которая выполняет операцию деления. Мы хотим убедиться, что эта функция вызывает исключение ZeroDivisionError, когда знаменатель равен нулю.

Реализация этой функции divide:

def divide(numerator, denominator):
    if denominator == 0:
        raise ZeroDivisionError("Denominator cannot be zero")
    return numerator / denominator
def divide(numerator, denominator):<br>if denominator == 0:<br>raise ZeroDivisionError("Denominator cannot be zero")<br>return numerator / denominator

Теперь давайте напишем тест-кейс, чтобы проверить, что divide вызывает ZeroDivisionError когда знаменатель равен нулю:

import unittest
from math_functions import divide

class TestMathFunctions(unittest.TestCase):

    def test_divide_by_zero(self):
        # Используем assertRaises чтобы проверить, вызывает ли функция devide исключение ZeroDivisionError
        with self.assertRaises(ZeroDivisionError):
            divide(10, 0)

if __name__ == '__main__':
    unittest.main()

Когда мы запускаем этот тест-кейс через unittest.main(), выполняется test_divide_by_zero, который проверяет, вызывает ли функция divide ожидаемое исключение ZeroDivisionError. Если тест пройден успешно, это означает, что функция divide правильно обрабатывает деление на ноль. Если тест провален, значит функция не вызывает ожидаемого исключения, что указывает на потенциальную ошибку в коде.

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

Основные методы и функции для тестирования в unittest

1. setUp() и tearDown()

Это специальные методы, которые вызываются до и после каждого из тестов. Они используются для настройки и удаления ресурсов, необходимых для тестирования. setUp() обычно используется для инициализации объектов или ресурсов, необходимых для тестирования, в то время как tearDown() для освобождения или очистки этих ресурсов после завершения теста.

def setUp(self):
    # Инициализация ресурсов
    self.database = Database()
    self.user = User(name='Alice')

def tearDown(self):
    # Очистка ресурсов
    self.database.close_connection()

2. setUpClass() и tearDownClass()

Используются для настройки и завершения тестов на уровне класса. Вызываются один раз перед выполнением всех тестов в классе, и после их завершения. Они полезны для настройки ресурсов, которые могут использоваться в нескольких тестах.

@classmethod
def setUpClass(cls):
    # Инициализация ресурсов, которые будут использоваться всеми тестами в классе
    cls.database = Database()
    cls.logger = Logger()

@classmethod
def tearDownClass(cls):
    # Очистка ресурсов, после прогона всех тестов
    cls.database.close_connection()
    cls.logger.close()

3. Пропуск тестов

Можно пропускать нужные нам тесты, используя декоратор @unittest.skip или методы unittest.skipIf() и unittest.skipUnless().

@unittest.skip("Skipping this test for now")
def test_something(self):
    # Test code here

@unittest.skipIf(sys.platform == "win32", "Skipping on Windows platform")
def test_another_thing(self):
    # Test code here

@unittest.skipUnless(has_network_connection(), "Skipping without network connection")
def test_network_operation(self):
    # Test code here

4. Параметризованные тесты

Можно запускать один и тот же тест с разными наборами входных параметров используя параметризацию. Это делается с помощью декоратора  @unittest.parameterized.parameterized или метод subTest().

@parameterized.parameterized.expand([
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
])
def test_addition(self, a, b, expected_result):
    result = add(a, b)
    self.assertEqual(result, expected_result)

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

Пример использования subTest():

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

def multiply(a, b):
    return a * b

Теперь давайте напишем тест-кейс с помощью subTest() для проверки функции multiply с несколькими комбинациями ввода-вывода:

import unittest
from math_functions import multiply

class TestMathFunctions(unittest.TestCase):

    def test_multiply(self):
        test_cases = [
            (2, 3, 6),
            (0, 5, 0),
            (-2, 4, -8),
            (10, -3, -30),
        ]

        # Перебор тест-кейсов
        for a, b, expected_result in test_cases:
            with self.subTest(a=a, b=b):
                result = multiply(a, b)
                self.assertEqual(result, expected_result)

if __name__ == '__main__':
    unittest.main()

Если какой-либо из подтестов провалится,  unittest сообщит об этом, но продолжит выполнение оставшихся подтестов. Это позволяет находить сразу несколько ошибок за один прогон, а не останавливаться при первой ошибке.

Использование subTest() помогает улучшить детализацию в автотестах и дает понимание, какие конкретно комбинации входных/выходных данных не прошли тест.

5. Распознавание тестов

Unittest автоматически распознает и запускает тест-кейсы из модулей и пакетов с помощью интерфейса командной строки unittest или через запуск unittest.main() без указания конкретных тест-кейсов.

python -m unittest discover

6. Тест-сьюты

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

suite = unittest.TestSuite()
suite.addTest(TestMathFunctions('test_add_positive_numbers'))
suite.addTest(TestMathFunctions('test_add_negative_numbers'))
runner = unittest.TextTestRunner()
runner.run(suite)

В следующем примере рассмотрим использование различных методов при тестировании с подключением к базе данных.

import unittest
import sqlite3
from parameterized import parameterized

class TestDatabase(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        # Подключение к находящейся в оперативной памяти базе данных SQLite
        cls.conn = sqlite3.connect(':memory:')
        cls.cursor = cls.conn.cursor()

        # Создание таблицы для тестирования
        cls.cursor.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')

    @classmethod
    def tearDownClass(cls):
        # Отключение от базы данных
        cls.conn.close()

    def setUp(self):
        # Начало транзакции
        self.conn.execute('BEGIN')

    def tearDown(self):
        # Откат транзакции
        self.conn.rollback()

    @parameterized.expand([
        ('Alice',),
        ('Bob',),
        ('Charlie',),
    ])
    def test_insert_user(self, name):
        # Вставка пользователя в базу данных
        self.cursor.execute("INSERT INTO users (name) VALUES (?)", (name,))
        self.conn.commit()

        # Проверка, что пользователь был добавлен корректно
        self.cursor.execute("SELECT * FROM users WHERE name=?", (name,))
        user = self.cursor.fetchone()
        self.assertIsNotNone(user)
        self.assertEqual(user[1], name)

    def test_delete_user(self):
        # Вставка пользователя в базу данных
        self.cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
        self.conn.commit()

        # Удаление пользователя из базы данных
        self.cursor.execute("DELETE FROM users WHERE name='Bob'")
        self.conn.commit()

        # Проверка, что пользователь был удален
        self.cursor.execute("SELECT * FROM users WHERE name='Bob'")
        user = self.cursor.fetchone()
        self.assertIsNone(user)

if __name__ == '__main__':
    unittest.main()

Импортируем модуль unittest для создания тест-кейсов, модуль sqlite3 для взаимодействия с базами данных SQLite и модуль parameterized для параметризированного тестирования.

import unittest
import sqlite3
from parameterized import parameterized

Определяем тест-кейс класс TestDatabase, который наследуется от unittest.TestCase. Классы тест-кейсов содержат отдельные методы тестов, которые определяют, какие тесты будут выполняться.

class TestDatabase(unittest.TestCase):

Мы определяем методы классов setUpClass()и tearDownClass() с помощью декоратора @classmethod. setUpClass() вызывается единожды перед выполнением всех тестов в классе, а tearDownClass() вызывается один раз после выполнения всех тестов в классе. В setUpClass() мы устанавливаем соединение с базой данных SQLite, находящейся в оперативной памяти (:memory:), и создаем таблицу с именем users для хранения информации о пользователях. В tearDownClass() мы закрываем соединение с базой данных.

 @classmethod
 def setUpClass(cls):
     cls.conn = sqlite3.connect(':memory:')
     cls.cursor = cls.conn.cursor()
     cls.cursor.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')

 @classmethod
 def tearDownClass(cls):
     cls.conn.close()

Определяем экземпляры методов setUp()и tearDown() без использования декоратора @classmethod. Эти методы вызываются до и после каждого метода теста в классе. Внутри setUp() начинаем транзакцию (BEGIN), чтобы изолировать изменения, вносимые каждым методом теста. Внутри tearDown() мы откатываем транзакцию (ROLLBACK), чтобы отменить все изменения, сделанные во время работы метода теста.

 def setUp(self):
     self.conn.execute('BEGIN')

 def tearDown(self):
     self.conn.rollback()

Здесь мы определяем метод теста test_insert_user и используем декоратор @parameterized.expand() для параметризации. Это позволит нам запускать тест с несколькими наборами входных параметров. В рамках теста мы добавляем пользователя с указанным именем в таблицу users, фиксируем транзакцию (.commit()), и затем извлекаем вставленного пользователя из базы данных. Мы используем ассерты, для проверки корректности добавления пользователя.

 @parameterized.expand([
     ('Alice',),
     ('Bob',),
     ('Charlie',),
 ])
 def test_insert_user(self, name):
     self.cursor.execute("INSERT INTO users (name) VALUES (?)", (name,))
     self.conn.commit()
     self.cursor.execute("SELECT * FROM users WHERE name=?", (name,))
     user = self.cursor.fetchone()
     self.assertIsNotNone(user)
     self.assertEqual(user[1], name)

Далее определяем еще один тест-метод test_delete_user для проверки удаления пользователя из базы данных. Внутри метода мы вставляем пользователя ‘Bob’ в таблицу users, удаляем его и затем проверяем, что пользователь был удален из базы данных.

 def test_delete_user(self):
     self.cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
     self.conn.commit()
     self.cursor.execute("DELETE FROM users WHERE name='Bob'")
     self.conn.commit()
     self.cursor.execute("SELECT * FROM users WHERE name='Bob'")
     user = self.cursor.fetchone()
     self.assertIsNone(user)

Используем условие if __name__ == '__main__':, чтобы проверить, запускается ли скрипт напрямую. Если да, вызываем unittest.main() для запуска тест-кейсов, определенных в классе TestDatabase.

if __name__ == '__main__':
    unittest.main()

Заключение

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

С помощью unittest мы можем использовать различные методы ассертов для проверки корректности кода, устанавливать и удалять данные для каждого тест-кейса и даже параметризировать тесты для запуска с различными входными данными. Кроме того, возможности обнаружения и пропуска тестов, создания пользовательских тест-сьютов, обеспечивают гибкость и масштабируемость для тестирования проектов любых размеров.

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

Перевод статьи «Automating Testing in Python using Unittest framework».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *