Введение
Этим уроком открывается цикл практических занятий по написанию тестов с применением различных инструментов.
unittest - инструмент для тестирования в Python. Это стандартный модуль для написания юнит-тестов на Python. Unittest это порт JUnit с Java. Иными словами, и в коде модуля, и при написании тестов легко прослеживается ООП стиль, что весьма удобно для тестирования процедур и классов.
Документация доступна по следующим ссылкам: python3, python2
В данном инструменте много возможностей: проверки (assert), декораторы, позволяющие пропустить отдельный тест (@skip, *@skipIf*) или обозначить сломанные тесты (@expectedFailure*) и этим не заканчивается список. Использование assert'ов с лихвой покрывает нужды при написании тестов.
Описание unittest
Полезная черта unittest - автоматизированное тестирование. Есть и другие:
- можно собирать тесты в группы
- собирать результаты выполнения тестов (например, для отчета)
- ООП стиль позволяет уменьшить дублирование кода при схожих объектах тестирования
В использовании unittest присутствуют несколько концепций
test case
test case -это наименьшая единица тестирования. Он проверяет конкретный ответ для конкретного набора входных данных.
test suite
test suite представляет собой сборник тестовых случаев, тестовых наборов. Используется для агрегирования тестов, которые должны выполняться вместе.
test fixture
test fixture - это фиксированное состояние объектов используемых в качестве исходного при выполнении тестов.
Цель использования fixture - если у вас сложный test case, то подготовка нужного состояния легко может занимать много ресурсов (например, вы считаете функцию с определенной точностью и каждый следующий знак точности в расчетах занимает день). Используя fixture (на сленге - фикстуры) предварительную подготовку состояния пропускаем и сразу приступаем к тестированию.
Test fixture может выступать, например, в виде:
- состояние базы данных
- набор переменных среды
- набор файлов с необходимым содержанием.
test runner
test runner - это компонент, который организует выполнение тестов и предоставляет результат пользователю.
Рекомендации к написанию тестов
При написании тестов следует исходить из следующих принципов:
- Работа теста не должна зависеть от результатов работы других тестов.
- Тест должен использовать данные, специально для него подготовленные, и никакие другие.
- Тест не должен требовать ввода от пользователя
- Тесты не должны перекрывать друг друга (не надо писать одинаковые тесты 20 раз). Можно писать частично перекрывающие тесты.
- Нашел баг -> напиши тест
- Тесты надо поддерживать в рабочем состоянии
- Модульные тесты не должны проверять производительность сущности (класса, функции)
- Тесты должны проверять не только то, что сущность работает корректно на корректных данных, но и то что ведет себя адекватно при некорректных данных.
- Тесты надо запускать регулярно
Практика
К написанию тестов стоит относится также как и к основному коду.
Написание тестов является хорошей инвестицией в будущее программы:
- Когда ваша программа становится настолько большой, что не помещается целиком у вас в голове, то это отличный звоночек, что стоит покрывать все тестами
- Если сейчас ваша программа не испытывает проблем, то через какое-то время библиотеки, которые вы используете, могут начать обновляться без обратной совместимости. Вот здесь-то тесты помогут
- Когда вы занимаетесь рефакторингом кода - тесты помогут не сломать лишнего
Есть и другие причины писать тесты. В целом, практика показывает, что до тестов надо дорасти - в какой-то момент приходит понимание зачем же тратить на них время.
В качестве примеров использования unittest продемонстрирую и опишу основные возможности модуля. На мой взгляд это те 20% которые помогут сделать 80% результата.
Пример синтаксиса №1
Рассмотрим следующий код:
# -*- encoding: utf-8 -*-
import unittest
class TestUM(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_numbers_3_4(self):
self.assertEqual(3 * 4, 12)
def test_strings_a_3(self):
self.assertEqual('a' * 3, 'aaa')
if __name__ == '__main__':
unittest.main()
В данном примере показан общий шаблон для большинства тестов - здесь и наследование от TestCase, здесь и два простых теста, а также перегрузка встроенных в TestCase методов:
- Метод def setUp(self) вызывается ПЕРЕД каждым тестом.
- Метод def tearDown(self) вызывается ПОСЛЕ каждого теста
Список подобных готовых функций такой:
- setUp – подготовка прогона теста; вызывается перед каждым тестом.
- tearDown – вызывается после того, как тест был запущен и результат записан. Метод запускается даже в случае исключения (exception) в теле теста.
- setUpClass – метод вызывается перед запуском всех тестов класса.
- tearDownClass – вызывается после прогона всех тестов класса.
- setUpModule – вызывается перед запуском всех классов модуля.
- tearDownModule – вызывается после прогона всех тестов модуля.
Если запустить скрипт:
python example.py
То получим:
python example1.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Теперь мы знаем как писать тест, как запускать. Перейдем к освещению вариантов использования тестов.
Пример синтаксиса №2 - №...
# -*- encoding: utf-8 -*-
import unittest
class TestUM(unittest.TestCase):
def testAssertTrue(self):
"""
Вызывает ошибку если значение аргумента != True
:return:
"""
self.assertTrue(True)
def testFailUnless(self):
"""
Устаревшее название для assertTrue()
Вызывает ошибку если значение аргумента != True
:return:
"""
self.failUnless(True)
def testFailIf(self):
"""
Устаревшая функция, теперь называется assertFalse()
:return:
"""
self.failIf(False)
def testAssertFalse(self):
"""
Если значение аргумент != False, то кидает ошибку
:return:
"""
self.assertFalse(False)
def testEqual(self):
"""
Проверка равенства двух аргументов
:return:
"""
self.failUnlessEqual(1, 3 - 2)
def testNotEqual(self):
"""
Проверка НЕ равенства двух аргументов
:return:
"""
self.failIfEqual(2, 3 - 2)
def testEqualFail(self):
"""
Ругается если значение аргументов равно
:return:
"""
self.failIfEqual(1, 2)
def testNotEqualFail(self):
"""
Ругается если значение аргументов не равно
:return:
"""
self.failUnlessEqual(2, 3 - 1)
def testNotAlmostEqual(self):
"""
Старое название функции.
Теперь называется assertNotAlmostEqual()
Сравнивает два аргумента с округлением, можно задать это округление
Ругается если значения равны
:return:
"""
self.failIfAlmostEqual(1.1, 3.3 - 2.0, places=1)
def testAlmostEqual(self):
"""
Старое название функции
Теперь называется assertAlmostEqual()
Сравнивает два аргумента с округлением, можно задать это округление
Ругается если значения не равны
:return:
"""
self.failUnlessAlmostEqual(1.1, 3.3 - 2.0, places=0)
if __name__ == '__main__':
unittest.main()
Прямо в док-строках описал когда выбрасывают ошибки эти проверки. В примере показаны как новые названия, так и их старые аналоги (в старых проектах такие еще встречаются)
Практические примеры
Перейдем к более практическим примерам. На них и рассмотрим еще некоторые важные способности unittest
- Как тестировать конструктор?
- Как тестировать структуры данных?
- Как тестировать работу с БД?
Как тестировать конструктор?
Приведу пример, как можно протестировать конструктор
# -*- encoding: utf-8 -*-
import unittest
class MyClass(object):
def __init__(self, foo):
if foo != 1:
raise ValueError("foo is not equal to 1!")
class MyClass2(object):
def __init__(self):
pass
class TestFoo(unittest.TestCase):
def testInsufficientArgs(self):
foo = 0
self.failUnlessRaises(ValueError, MyClass, foo)
def testArgs(self):
self.assertRaises(TypeError, MyClass2, ("fsa", "fds"))
if __name__ == '__main__':
unittest.main()
В коде видно, что есть класс, в конструкторе которого кидается исключение если значение аргумента не удовлетворяет условию. Это условие ловится функцией self.assertRaises или ее старым названием self.failUnlessRaises
Во втором примере показан тест на количество аргументов в классе.
Как тестировать структуру данных?
Продемонстрирую пример тестирования структуры данных. В данном случае есть класс Point, в котором хранится два float’а x,y. А дальше весь код такой же как и для других тестов.
# -*- encoding: utf-8 -*-
import unittest
class Point(object):
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def __str__(self):
return f"({self.x}, {self.y})"
def __eq__(self, other):
return True if ((self.x == other.x) and (self.y == other.y)) else False
def __ne__(self, other):
return True if ((self.x != other.x) or (self.y != other.y)) else False
class TestPoint(unittest.TestCase):
def setUp(self):
self.A = Point(5, 6)
self.B = Point(6, 10)
self.C = Point(5.0, 6.0)
self.D = Point(-5, -6)
def test_init(self):
self.assertEqual((self.A.x, self.A.y), (float(5), float(6)), "Полученные значения не являются вещественными!!!")
self.assertEqual((self.B.x, self.B.y), (float(6), float(10)),
"Полученные значения не являются вещественными!!!")
self.assertEqual((self.C.x, self.C.y), (float(5), float(6)), "Полученные значения не являются вещественными!!!")
self.assertEqual((self.D.x, self.D.y), (float(-5), float(-6)),
"Полученные значения не являются вещественными!!!")
def test_str(self):
self.assertTrue(str(self.A) == "(5.0, 6.0)", "Неправильный вывод на экран!!!")
self.assertTrue(str(self.B) == "(6.0, 10.0)", "Неправильный вывод на экран!!!")
self.assertTrue(str(self.C) == "(5.0, 6.0)", "Неправильный вывод на экран!!!")
self.assertTrue(str(self.D) == "(-5.0, -6.0)", "Неправильный вывод на экран!!!")
def test_eq(self):
self.assertTrue(self.A == self.C,
"Данные две точки равны, а в результате тестирования, они оказались неравными!!!")
self.assertFalse(self.A == self.B,
"Данные две точки неравны, а в результате тестирования, они оказались равными!!!")
self.assertFalse(self.A == self.D,
"Данные две точки неравны, а в результате тестирования, они оказались равными!!!")
def test_ne(self):
self.assertFalse(self.A != self.C,
"Данные две точки равны, а в результате тестирования, они оказались неравными!!!")
self.assertTrue(self.A != self.B,
"Данные две точки неравны, а в результате тестирования, они оказались равными!!!")
self.assertTrue(self.A != self.D,
"Данные две точки неравны, а в результате тестирования, они оказались равными!!!")
if __name__ == '__main__':
unittest.main()
Как тестировать работу с БД?
Примера под тестирование БД не приведу, однако, расскажу об общих соображениях.
Работа с базой не так проста, как с обычной функцией, ведь база - это не просто программный код, база данных - это объект, сохраняющий своё состояние. И если мы начнём в процессе тестирования изменять данные в базе, то после каждого теста база будет изменяться. Это может помешать последующим тестам и необратимо испортить базу данных.
Ключ к решению проблемы - транзакции. Одна из особенностей этого механизма состоит в том, что до тех пор пока транзакция не завершена, вы всегда можете отменить все изменения и вернуть базу в состояние на момент начала транзакции. Алгоритм такой:
- открываем транзакцию;
- если нужно, выполняем подготовительные действия для тестирования;
- выполняем модульный тест (или просто запускаем сценарий, работу которого хотим проверить);
- проверяем результат работы сценария;
- отменяем транзакцию, возвращая базу данных в исходное состояние.
Даже если в тестируемом коде останутся незакрытые транзакции, внешний ROLLBACK всё равно откатит все изменения корректно.
Помимо транзакций стоит обратить внимание, что тестирование следует делать на тестовой базе данных. Идеальным случаем будет полная инициализация окружения перед исполнением теста.
Выводы
В уроке описаны базовые элементы работы с unittest. Не были затронуты статусы тестов, отчеты о тестировании. Однако, данной теории хватит для написания тестов. Давайте это сейчас и проверим, переходите к практике.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»