unittest

Содержание
Введение
Структура проекта
Тестируем решение квадратного уравнения
Теория для unittest
Похожие статьи

Введение

Unittest это библиотека для тестирования, которая входит в Python по умолчанию

Он содержит и тестовую среду, и Test Runner. У unittest есть ряд требований для написания и выполнения тестов:

*имеется в виду, что в 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

Поиск по сайту

Подпишитесь на Telegram канал @aofeed чтобы следить за выходом новых статей и обновлением старых

Перейти на канал

@aofeed

Задать вопрос в Телеграм-группе

@aofeedchat

Контакты и сотрудничество:
Рекомендую наш хостинг beget.ru
Пишите на info@urn.su если Вы:
1. Хотите написать статью для нашего сайта или перевести статью на свой родной язык.
2. Хотите разместить на сайте рекламу, подходящую по тематике.
3. Реклама на моём сайте имеет максимальный уровень цензуры. Если Вы увидели рекламный блок недопустимый для просмотра детьми школьного возраста, вызывающий шок или вводящий в заблуждение - пожалуйста свяжитесь с нами по электронной почте
4. Нашли на сайте ошибку, неточности, баг и т.д. ... .......
5. Статьи можно расшарить в соцсетях, нажав на иконку сети: