unittest
Введение | |
Структура проекта | |
Тестируем решение квадратного уравнения | |
Теория для unittest | |
Похожие статьи |
Введение
Unittest это библиотека для тестирования, которая входит в Python по умолчанию
Он содержит и тестовую среду, и Test Runner.
У unittest есть ряд требований для написания и выполнения тестов:
- Тесты нужно поместить в методы класса unittest.TestCase;
- Нужно использовать специальные методы утверждения из класса unittest.TestCase, а не встроенный оператор assert*.
*имеется в виду, что в unittest нужно для каждого типа утверждений использовать свой метод, например:
assertEqual | - чтобы утвердить равенство |
assertNotEqual | - чтобы утвердить неравенство |
assertTrue | - чтобы утвердить истинность |
И так далее, подробности на сайте
docs.python.org
Этим unittest отличается от, например,
PyTest
, где утверждение делается всегда одинаково - с помощью
assert
Разберём простейший пример использования.
Создадим рабочую директорию app,
файл
calc.py
в корне
и файл
test_calc.py
в поддиректории
tests
Также создадим и активируем
виртуальное окружение
python -m venv venv
source venv/bin/activate
Структура проекта:
app ├── calc.py ├── tests │ └── test_calc.py └── venv ├── bin ├── include ├── lib ├── lib64 -> lib └── pyvenv.cfg
# calc.py
def add(x, y):
"""Add Function"""
return x + y + 3
def subtract(x, y):
"""Subtract Function"""
return x - y
def multiply(x, y):
"""Multiply Function"""
return x * y
def divide(x, y):
"""Divide Function"""
if y == 0:
raise ValueError("Can not divide by zero!")
return x / y
Как видите, в функции add() специально допущена ошибка - вместо сложения двух переменных к ним ещё добавляется число 3
# test_calc.py
import unittest
import calc
# https://docs.python.org/3/library/unittest.html#unittest.TestCase.debug
class TestCalc(unittest.TestCase):
def test_add(self):
result = calc.add(10, 5)
self.assertEqual(result, 15)
# python3 -m unittest tests/test_calc.py
Для запуска теста перейдём в директорию с файлами и в консоли выполним команду
python -m unittest tests/test_calc.py
F ====================================================================== FAIL: test_add (tests.test_calc.TestCalc) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/andrei/python/unittest/app/tests/test_calc.py", line 12, in test_add self.assertEqual(result, 15) AssertionError: 18 != 15 ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
Исправим ошибку
# calc.py
def add(x, y):
"""Add Function"""
return x + y
...
python3 -m unittest test_calc.py
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Чтобы запустить этот тест в PyCharm или запускать его из консоли, но без дополнительного указания -m unittest добавим в конец файла test_calc.py две строчки:
# test_calc.py
...
if __name__ == '__main__':
unittest.main()
Теперь можно запускать тест командой
python -m tests.test_calc
Напишем тесты для всех функций из calc.py
Снова специально допустим ошибку, например, в третьем тесте
import unittest
import calc
class TestCalc(unittest.TestCase):
def test_add(self):
self.assertEqual(calc.add(10, 5), 15)
self.assertEqual(calc.add(-1, 1), 0)
self.assertEqual(calc.add(-1, -1), -2)
def test_subtract(self):
self.assertEqual(calc.subtract(10, 5), 5)
self.assertEqual(calc.subtract(-1, 1), -2)
self.assertEqual(calc.subtract(-1, -1), 0)
def test_multiply(self):
self.assertEqual(calc.multiply(10, 5), 70)
self.assertEqual(calc.multiply(-1, 1), -1)
self.assertEqual(calc.multiply(-1, -1), 1)
def test_divide(self):
self.assertEqual(calc.divide(10, 5), 2)
self.assertEqual(calc.divide(-1, 1), -1)
self.assertEqual(calc.divide(-1, -1), 1)
if __name__ == '__main__':
unittest.main()
Запустим тест
python3 test_calc.py
..F. ====================================================================== FAIL: test_multiply (__main__.TestCalc) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:/Users/username/.PyCharmCE2018.3/config/scratches/test_calc.py", line 17, in test_multiply self.assertEqual(calc.multiply(10, 5), 70) AssertionError: 50 != 70 ---------------------------------------------------------------------- Ran 4 tests in 0.001s FAILED (failures=1) Process finished with exit code 1
Обратите внимание на первую строчку, точки означают успешное выполнение теста. F - провал теста.
..F. означает, что первый, второй и четвёртый тесты прошли успешно, а в третьем ошибка
Assertion который вернул FALSE также видно
self.assertEqual(calc.multiply(10, 5), 70)
И ошибка AssertionError: 50 != 70
Имея такой подробный результат мы легко исправляем ошибку
self.assertEqual(calc.multiply(10, 5), 70)
Меняем на
self.assertEqual(calc.multiply(10, 5), 50)
Запустим тест
python3 test_calc.py
.... ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK Process finished with exit code 0
Тестируем решение квадратного уравнения
Создадим ещё два файла quadratic.py и test_quadratic.py
Структура проекта
app ├── calc.py ├── __pycache__ ├── quadratic.py ├── tests │ ├── __pycache__ │ ├── test_calc.py │ └── test_quadratic.py └── venv ├── bin ├── include ├── lib ├── lib64 -> lib └── pyvenv.cfg
Квадратные уравнения это уравнения вида
a*x^2 + b*x + c
x^2 и x подразумеваются по умолчанию, поэтому достаточно задать a, b и c - и сразу станет понятно как выглядит квадратное уравнение.
Первым делом проверим, что a, b и c это числа
def quadratic_solve(a ,b, c): if not all( map( lambda p: isinstance(p, (int, float)), (a, b, c) ) ): raise TypeError("Not valid argument type") print("Types are OK")
Здесь я использовал функции:
all()
,
map()
и
лямбда функцию
Если что-то неясно - перейдите по ссылкам на функции либо посетите раздел
«Функции»
Пример теста
# test_quadratic.py import unittest from quadratic import quadratic_solve class TestQuadratic(unittest.TestCase): def test_raises_type_error(self): try: # Специально передаём строку quadratic_solve("", 1, 1.5) except TypeError as err: print("\nOK, caught type error", err) else: self.fail("NOK, missed type error")
python -m unittest -v tests/test_quadratic.py
test_raises_type_error (tests.test_quadratic.TestQuadratic) ... OK, caught type error Not valid argument type ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Тест можно переписать в более нативном виде - без try и except. unittest уже продумал такую ситуацию
def test_raises_type_error(self):
with self.assertRaises(TypeError):
quadratic_solve("", 1, 1.5)
Теперь можно дописать код решения квадратного уравнения
from math import sqrt def quadratic_solve(a ,b, c): if not all( map( lambda p: isinstance(p, (int, float)), (a, b, c) ) ): raise TypeError("Not valid argument type") print("Types are OK") if a == 0: if b == 0: # a и b 0: решения нет return None, None return -c / b, None d = b ** 2 - 4 * a * c if d < 0: return None, None d_root = sqrt(d) divider = 2 * a x1 = (-b + d_root) / divider x2 = (-b - d_root) / divider if d == 0: x2 = None elif x2 > x1: x1, x2 = x2, x1 return x1, x2
И написать тесты на само решение
# test_quadratic.py import unittest from quadratic import quadratic_solve class TestQuadratic(unittest.TestCase): def test_raises_type_error(self): with self.assertRaises(TypeError): quadratic_solve("", 1, 1.5) def test_result_is_tuple(self): res = quadratic_solve(0, 0, 0) self.assertIsInstance(res, tuple) def test_zero_a_and_b(self): res = quadratic_solve(0, 0, 1) self.assertEqual(res, (None, None)) def test_two_roots(self): res = quadratic_solve(1, -1, -2) self.assertEqual(res, (2.0, -1.0)) def test_single_root(self): res = quadratic_solve(1, -2, 1) self.assertEqual(res, (1.0, None))
test_raises_type_error (tests.test_quadratic.TestQuadratic) ... ok test_result_is_tuple (tests.test_quadratic.TestQuadratic) ... Types are OK ok test_single_root (tests.test_quadratic.TestQuadratic) ... Types are OK ok test_two_roots (tests.test_quadratic.TestQuadratic) ... Types are OK ok test_zero_a_and_b (tests.test_quadratic.TestQuadratic) ... Types are OK ok ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
Все тесты успешно проходят. Тем не менее такой набор тестов имеет избыточный код.
Несколько тестов состоят в том, что в одну и ту же функцию передаётся какое-то значение
и затем результат сравнивается с эталоном. Избавиться от лишнего кода поможет параметризация
тестов. С этим хорошо справляется
PyTest
Если вам нужны примеры квадратных уравнений с уже вычисленными корнями - их можно найти здесь
Теория для unittest
Для тех, кто интересуется устарел ил unittest или нет, моё скромное мнение состоит в том, что нет.
В мире Python более модным считается
PyTest
одна из причин - более простой синтаксис.
Тем не менее
unittest удобен тем, что он встроен в стандартную библиотеку и тем, что
в других языках программирования тоже есть похожие фреймворки.
Test Fixture
Единичный тест называется тест кейсом (Test Case).
Часто нужно запустить несколько тест-кейсов, которым требуется для запуска одно и то же.
Например, получить один и тот же объект или запустить
Selenium Webdriver
или что-то другое.
Чтобы не писать в каждом тест-кейсе одно и то же можно воспользоваться методами
setUp и tearDown которые создаются один раз для каждого класса и будут запускаться перед
каждым тест-кейсом.
Такая комбинация setUp + tearDown называется
Test Fixture
Test Fixture = setUp + tearDown
Составляющие части любого теста
Порядок выполнения тестов обычно следующий:
Подготовка к тесту
Непосредственное действие, например, запуск определённой функции
Проверка результата на соответствие ожиданию.
Arrange → Act → Assert
Длинных тестов в которых происходит множество чередующихся действий и проверок
следует по возможности избегать.
Лучше написать несколько небольших тестов чем один длинный. Тогда при фэйле какого-то из
хорошо структурированных тестов будет легче понять, что именно не работает.
Тестирование | |
Python |