Сегодня, в условиях постоянно развивающейся разработки ПО, обеспечение надежности и стабильности кодовой базы имеет первостепенное значение. Одним из наиболее эффективных способов достижения этих целей является автоматизированное тестирование. Автоматизация тестирования позволяет систематически проверять корректность кода, выявлять баги на ранней стадии и поддерживать качество программного обеспечения на протяжении всего жизненного цикла.
Python, благодаря своей простоте и универсальности, предоставляет мощные инструменты для автоматизации тестирования. Среди них фреймворк unittest
выделяется как надежное и многофункциональное решение для написания и запуска автотестов. В данной статье мы рассмотрим основы автоматизации тестирования на Python с unittest
.
Мы начнем с понимания основ модульного тестирования и того, как оно вписывается в процесс разработки программного обеспечения. Затем мы углубимся во фреймворк unittest
, научимся писать тест-кейсы, использовать ассерты для проверки ожидаемых результатов и группировать тесты в тест-сьюты.
Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.
Смотрите также: “Основы автоматизации с помощью Selenium и Python”.
Предположим, у вас есть простой скрипт на Python с функцией, которая складывает два числа. Вот этот скрипт (math_functions.py
):
def add(a, b): return a + b
Теперь давайте напишем тест-кейсы для этого скрипта, используя unittest
. Мы создадим класс теста, который наследуется от unittest.TestCase
и определим в нем методы тестов.
- Импортируем
unittest
и функциюadd
из нашего скрипта (math_functions.py
). - Создаем класс с именем
TestMathFunctions
который наследуется отunittest.TestCase
. Каждый тест-кейс – это метод в внутри класса. - Убеждаемся, что название каждого теста начинается с
test_
, что является соглашением об именовании, признаннымunittest
для идентификации методов тестирования. - Внутри каждого метода тестирования мы используем ассерты по типу
assertEqual
для проверки ожидаемого поведения нашей функцииadd
. - Наконец, мы используем
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
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
идентично b
(проверяет, ссылаются ли 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».