Раздел «Язык Си».PythonUnittest:

unittest

Документация

Плюсы unittest

Почему unittest? Потому что

Что будем тестировать

Тестируем модуль calc.py

def add(a, b):
    return a + b
    
def sub(a, b):
    return a-b
 
def mul(a, b):
    return a * b
 
def div(a, b):
    return a / b

Напишем тесты, используя unittest и сохраним в файле test_calc.py

import unittest
import calc
 
class CalcTest(unittest.TestCase):
    def test_add(self):
        self.assertEqual(calc.add(1, 2), 3)
        
    def test_sub(self):
        self.assertEqual(calc.sub(4, 2), 2)
        
    def test_mul(self):
        self.assertEqual(calc.mul(2, 5), 10)
        
    def test_div(self):
        self.assertEqual(calc.div(8, 4), 2)
        
if __name__ == '__main__':
    unittest.main()

Запускаем:

python test_calc.py

Рекомендуем, однако, запускать следующим образом:

python -m unittest test_calc.py
Так вы легко в командной строке запустите все тесты в файле, все тесты в классе, один тест или вообще все файлы с тестами (см. далее).

Получили:

....
----------------------------------------------------------------------
Ran 4 tests in 0.003s

OK

Лаконично. Запустим с ключом -v (verbose) и получим отчет по каждому тесту.

test_add (test_calc.CalcTest) ... ok
test_div (test_calc.CalcTest) ... ok
test_mul (test_calc.CalcTest) ... ok
test_sub (test_calc.CalcTest) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.004s

OK

Как мы придумали название класса и методов?

ИмяТестируемойСущностиTests.ИмяТестируемойСущности – это некоторая логическая единица, тесты для которой нужно написать. В нашем случае – это калькулятор, поэтому мы выбрали имя CalcTests?. Если бы у нашего калькулятора был большой набор поддерживаемых функций, то тестирование простых функций (сложение, вычитание, умножение и деление) можно было бы вынести в отдельный класс и назвать его например так: CalcSimpleActionsTests?.

Терминология

Вся работа по написанию тестов заключается в том, что мы разрабатываем отдельные тесты в рамках test case’ов, собираем их в модули и запускаем, если нужно объединить несколько test case’ов, для их совместного запуска, они помещаются в test suite’ы, которые помимо test case’ов могут содержать другие test suite’ы.

Запуск

Из командной строки (CLI - command line interface)

REFACTOR не забываем ключ -v (verbose)

Всего файла

python -m unittest test_calc.py

Одного класса из файла

python -m unittest test_calc.CalcTest

Одного теста

python -m unittest test_calc.CalcTest.test_sub

REFACTOR Запуск всех тестов, что найдет TestDiscovery?

python -m unittest
python -m unittest discover
Эти формы запуска эквивалентны.

По умолчанию из текущей директории находятся все файлы с маской test*.py и запускаются.

Если нужно запустить из другой директории или с другой маской, используйте

python -m unittest discover project_directory "*_test.py"

GUI

Иногда проще поставить GUI, чем обучать людей. Один из вариантов - поставить Cricket

Установка:
pip install cricket

Запуск:
cricket-unittest

Все разнообразие вариантов: https://wiki.python.org/moin/PythonTestingToolsTaxonomy#GUI_Testing_Tools

Методы

Для того, чтобы метод класса выполнялся как тест, необходимо, чтобы он начинался со слова test. Несмотря на то, что методы framework’а unittest написаны не в соответствии с PEP 8 (ввиду того, что идейно он наследник xUnit), мы все же рекомендуем следовать правилам стиля для Python везде, где это возможно. Поэтому имена тестов будем начинать с префикса test_. Далее, под словом тест будем понимать метод класса-наследника от TestCase?, который начинается с префикса test_.

Методы класса unittest.TestCase:

Методы, используемые при запуске тестов (setUp, tearDown)

python-unittest-part2-3.png

@classmethod
def setUpClass(cls):
   # что нужно сделать перед всеми тестами этого класса

@classmethod
def tearDownClass(cls):
   # что нужно сделать после окончания всех тестов этого класса

По умолчанию все эти методы ничего не делают.

Assert'ы

fail(msg = None) - в тесте произошла ошибка.

Проверка и генерация ошибок:

assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)

Сравнения и поиск:

assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertRegex(s, r) r.search(s)
assertNotRegex(s, r) not r.search(s)
assertCountEqual(a, b) a и b имеют одинаковые элементы (порядок неважен)

Типо-зависимые assert’ы, которые используются при вызове assertEqual(). Приводятся на тот случай, если необходимо использовать конкретный метод.

assertMultiLineEqual(a, b) строки (strings)
assertSequenceEqual(a, b) последовательности (sequences)
assertListEqual(a, b) списки (lists)
assertTupleEqual(a, b) кортежи (tuplse)
assertSetEqual(a, b) множества или неизменяемые множества (frozensets)
assertDictEqual(a, b) словари (dicts)

Контроль выбрасываемых исключений и warning'ов (да, их тоже нужно проверить!)

assertRaises(exc, fun, *args, **kwds) Функция fun(*args, **kwds) вызывает исключение exc
assertRaisesRegex(exc, r, fun, *args, **kwds) Функция fun(*args, **kwds) вызывает исключение exc, сообщение которого совпадает с регулярным выражением r
assertWarns(warn, fun, *args, **kwds) Функция fun(*args, **kwds) выдает сообщение warn
assertWarnsRegex(warn, r, fun, *args, **kwds) Функция fun(*args, **kwds) выдает сообщение warn и оно совпадает с регулярным выражением r

Методы, позволяющие собирать информацию о самом тесте

Пример использования методов

Добавим методы в test_calc.py

import unittest
import calc


class CalcTest(unittest.TestCase):
   """Calc tests"""

   @classmethod
   def setUpClass(cls):
       """Set up for class"""
       print("setUpClass")
       print("==========")

   @classmethod
   def tearDownClass(cls):
       """Tear down for class"""
       print("==========")
       print("tearDownClass")

   def setUp(self):
       """Set up for test"""
       print("Set up for [" + self.shortDescription() + "]")

   def tearDown(self):
       """Tear down for test"""
       print("Tear down for [" + self.shortDescription() + "]")
       print("")

   def test_add(self):
       """Add operation test"""
       print("id: " + self.id())
       self.assertEqual(calc.add(1, 2), 3)

   def test_sub(self):
       """Sub operation test"""
       print("id: " + self.id())
       self.assertEqual(calc.sub(4, 2), 2)

   def test_mul(self):
       """Mul operation test"""
       print("id: " + self.id())
       self.assertEqual(calc.mul(2, 5), 10)

   def test_div(self):
       """Div operation test"""
       print("id: " + self.id())
       self.assertEqual(calc.div(8, 4), 2)


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

При запуске python -m unittest -v test_calc.py получим

setUpClass
==========
test_add (simple_ex.CalcTest)
Add operation test ... Set up for [Add operation test]
id: simple_ex.CalcTest.test_add
Tear down for [Add operation test]
ok

test_div (simple_ex.CalcTest)
Div operation test ... Set up for [Div operation test]
id: simple_ex.CalcTest.test_div
Tear down for [Div operation test]
ok

test_mul (simple_ex.CalcTest)
Mul operation test ... Set up for [Mul operation test]
id: simple_ex.CalcTest.test_mul
Tear down for [Mul operation test]
ok

test_sub (simple_ex.CalcTest)
Sub operation test ... Set up for [Sub operation test]
id: simple_ex.CalcTest.test_sub
Tear down for [Sub operation test]
ok
==========
tearDownClass
----------------------------------------------------------------------
Ran 4 tests in 0.016s
OK

Класс TestSuite?

Вспомним calc.py и test_calc.py.

Напишем пускалку тестов - файл test_runner.py

import unittest
import calc_tests

calcTestSuite = unittest.TestSuite()
calcTestSuite.addTest(unittest.makeSuite(calc_tests.CalcTest))

runner = unittest.TextTestRunner(verbosity=2)
runner.run(calcTestSuite)

Запуск:

python test_runner.py

Тестирование новой фукнциональности в старом классе

Допишем в файл calc.py новые функции. Простестируем старые функции (сначала) и потом запустим тесты новых функций (напишем новый класс).

def add(a, b):
    return a + b
    
def sub(a, b):
    return a-b
 
def mul(a, b):
    return a * b
 
def div(a, b):
    return a / b

def sqrt(a):             # новая функциональность
    return a**0.5

def pow(a, b):           # новая функциональность
    return a**b

Добавим тесты для новых функций, создав новый класс с именем CalcExTests? (расширенные функции калькулятора) с тестами для sqrt() и pow(), а класс CalcTest? переименуем в CalcBasicTests? (базовые функции калькулятора).

Модуль test_calc.py

import unittest
import calc
 
class CalcBasicTests(unittest.TestCase):
    def test_add(self):
        self.assertEqual(calc.add(1, 2), 3)
        
    def test_sub(self):
        self.assertEqual(calc.sub(4, 2), 2)
        
    def test_mul(self):
        self.assertEqual(calc.mul(2, 5), 10)
        
    def test_div(self):
        self.assertEqual(calc.div(8, 4), 2)


class CalcExTests(unittest.TestCase):
    def test_sqrt(self):
        self.assertEqual(calc.sqrt(4), 2)
        
    def test_pow(self):
        self.assertEqual(calc.pow(3, 3), 27)

Установим в test_runner порядок запуска наборов тестов:

import unittest
import calc_tests

calcTestSuite = unittest.TestSuite()
calcTestSuite.addTest(unittest.makeSuite(calc_tests.CalcBasicTests))
calcTestSuite.addTest(unittest.makeSuite(calc_tests.CalcExTests))
print("count of tests: " + str(calcTestSuite.countTestCases()) + "\n")

runner = unittest.TextTestRunner(verbosity=2)
runner.run(calcTestSuite)

Запускаем python test_runner.py

count of tests: 6
test_add (calc_tests.CalcBasicTests) ... ok
test_div (calc_tests.CalcBasicTests) ... ok
test_mul (calc_tests.CalcBasicTests) ... ok
test_sub (calc_tests.CalcBasicTests) ... ok
test_pow (calc_tests.CalcExTests) ... ok
test_sqrt (calc_tests.CalcExTests) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK

Сначала выполняются тесты из класса CalcBasicTests?, потом из CalcExTests?.

Загрузка и запуск тестов

Класс TestLoader?

TestLoader - используется для создания групп из классов и модулей

Модуль test_runner.py

import unittest
import calc_tests

testCases = []
testCases.append(calc_tests.CalcBasicTests)
testCases.append(calc_tests.CalcExTests)

testLoad = unittest.TestLoader()

suites = []
for tc in testCases:
    suites.append(testLoad.loadTestsFromTestCase(tc))

res_suite = unittest.TestSuite(suites )

runner = unittest.TextTestRunner(verbosity=2)
runner.run(res_suite)

Запустим.

$ python3 test_runner.py
test_add (calc_tests.CalcBasicTests) ... ok
test_div (calc_tests.CalcBasicTests) ... ok
test_mul (calc_tests.CalcBasicTests) ... ok
test_sub (calc_tests.CalcBasicTests) ... ok
test_pow (calc_tests.CalcExTests) ... ok
test_sqrt (calc_tests.CalcExTests) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.007s

OK

Еще пример файла test_runner.py:

import unittest
import calc_tests

testLoad = unittest.TestLoader()
suites = testLoad.loadTestsFromModule(calc_tests)

runner = unittest.TextTestRunner(verbosity=2)
runner.run(suites)

Запустим и получим:

test_add (calc_tests.CalcBasicTests) ... ok
test_div (calc_tests.CalcBasicTests) ... ok
test_mul (calc_tests.CalcBasicTests) ... ok
test_sub (calc_tests.CalcBasicTests) ... ok
test_pow (calc_tests.CalcExTests) ... ok
test_sqrt (calc_tests.CalcExTests) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.016s
OK

Если в модуле test_runner.py заменить строку

suites = testLoad.loadTestsFromModule(calc_tests)

на

suites = testLoad.loadTestsFromName('calc_tests.CalcBasicTests')

то будут выполнены только тесты из класса CalcBasicTests?.

test_add (calc_tests.CalcBasicTests) ... ok
test_div (calc_tests.CalcBasicTests) ... ok
test_mul (calc_tests.CalcBasicTests) ... ok
test_sub (calc_tests.CalcBasicTests) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.002s
OK

IDEA! Можно сделать разные наборы - для полного тестирования (например, на несколько часов или дней) и для быстрой проверки после каждой сборки.

Класс TestResult?

Класс TestResult? используется для сбора информации о результатах прохождения тестов.

Модифицируем файл test_runner.py, чтобы печатался только итог, но развернутый итог.

import unittest
import calc_tests

testLoad = unittest.TestLoader()
suites = testLoad.loadTestsFromModule(calc_tests)

testResult = unittest.TestResult()

runner = unittest.TextTestRunner(verbosity=1)   # понизим говорливость тестов
testResult = runner.run(suites)                 # станем анализировать, что вернул run
print("errors")
print(len(testResult.errors))
print("failures")
print(len(testResult.failures))
print("skipped")
print(len(testResult.skipped))
print("testsRun")
print(testResult.testsRun)

Запустим и получим:

......
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
errors
0
failures
0
skipped
0
testsRun
6

Получили расширенное summary.

Класс TextTestRunner?

Объекты класса TextTestRunner? используются для запуска тестов. Среди параметров, которые передаются конструктору класса, можно выделить verbosity, по умолчанию он равен 1, если создать объект с verbosity=2, то будем получать расширенную информацию о результатах прохождения тестов. Для запуска тестов используется метод run(), которому в качестве аргумента передается класс-наследник от TestCase? или группа (TestSuite?).

В наших примерах TextTestRunner? используется в модуле test_runner.py в строчках:

runner = unittest.TextTestRunner(verbosity=2)

testResult = runner.run(suites)

В первой строке создается объект класса TextTestRunner? с verbosity=2, а во второй строке запускаются тесты из группы suites, результат тестирования попадает в объект testResult, атрибуты которого можно анализировать в дальнейшем.

Пропуск тестов и классов

Зачем пропускать тесты? Например, у нас известный баг, который поломал нам часть тестов. Мы ведем разработку над другой частью, которая независима и не хотим вчитываться в результаты запуска тестов - это наши тесты сломались или известные чужие (если НЕ известные чужие, то мы вчитываемся разбираемся с поломками новых тестов).

Возьмем простой test_runner.py, который запускает все тесты из всех классов.

import unittest
import calc_tests

calcTestSuite = unittest.TestSuite()
calcTestSuite.addTest(unittest.makeSuite(calc_tests.CalcBasicTests))
calcTestSuite.addTest(unittest.makeSuite(calc_tests.CalcExTests))

runner = unittest.TextTestRunner(verbosity=2)
runner.run(calcTestSuite)

Для пропуска тестов можно закоментаривать часть тестов (нет об этом диагностики, долго). Но хотим быстрый способ НЕ делать часть тестов или классов тестов.

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

Добавьте к тесту декоратор

@unittest.skip(reason)

Например:

class CalcBasicTests(unittest.TestCase):
    @unittest.skip("Temporaly skip test_add")    
    def test_add(self):
        self.assertEqual(calc.add(1, 2), 3)
        
    def test_sub(self):
        self.assertEqual(calc.sub(4, 2), 2)
        
    def test_mul(self):
        self.assertEqual(calc.mul(2, 5), 10)
        
    def test_div(self):
        self.assertEqual(calc.div(8, 4), 2)

Запустим:

test_add (calc_tests.CalcBasicTests) ... skipped 'Temporarily skipped'

test_div (calc_tests.CalcBasicTests) ... ok

test_mul (calc_tests.CalcBasicTests) ... ok

test_sub (calc_tests.CalcBasicTests) ... ok

test_pow (calc_tests.CalcExTests) ... ok

test_sqrt (calc_tests.CalcExTests) ... ok

----------------------------------------------------------------------

Ran 6 tests in 0.003s

OK (skipped=1)

Заметьте, у нас есть информация, что все остальные тесты прошли хорошо, и что у нас есть специально пропущенные тесты (которые нужно будет в конце концов выполнить).

Условный пропуск тестов

Добавьте декоратор

@unittest.skipIf(condition, reason)
Тест будет пропущен, если условие (condition) истинно.

@unittest.skipUnless(condition, reason)
Тест будет пропущен если, условие (condition) не истинно.

Зачем это нужно?

Условный пропуск тестов можно использовать в ситуациях, когда те или иные тесты зависят от версии программы, например: в новой версии уже не поддерживается часть методов; или тесты могут быть платформозависимые, например: ряд тестов могут выполняться только под операционной системой MS Windows. Условие записывается в параметр condition, текстовое описание – в reason.

Пропуск классов

Используйте декоратор

@unittest.skip(reason)

который записывается перед объявлением класса. В результате все тесты из данного класса не будут выполнены. В рамках нашего примера с математическими действиями, для исключения из процесса тестирования методов sqrt и pow поместим декоратор skip перед объявлением класса CalcExTests?.

import unittest
import calc
 
class CalcBasicTests(unittest.TestCase):
    def test_add(self):
        self.assertEqual(calc.add(1, 2), 3)
        
    def test_sub(self):
        self.assertEqual(calc.sub(4, 2), 2)
        
    def test_mul(self):
        self.assertEqual(calc.mul(2, 5), 10)
        
    def test_div(self):
        self.assertEqual(calc.div(8, 4), 2)

@unittest.skip("Skip CalcExTests")
class CalcExTests(unittest.TestCase):
    def test_sqrt(self):
        self.assertEqual(calc.sqrt(4), 2)
        
    def test_pow(self):
        self.assertEqual(calc.pow(3, 3), 27)

Запустим все тесты test_runner.py и получим:

test_add (calc_tests.CalcBasicTests) ... ok

test_div (calc_tests.CalcBasicTests) ... ok

test_mul (calc_tests.CalcBasicTests) ... ok

test_sub (calc_tests.CalcBasicTests) ... ok

test_pow (calc_tests.CalcExTests) ... skipped 'Skip CalcExTests'

test_sqrt (calc_tests.CalcExTests) ... skipped 'Skip CalcExTests'

----------------------------------------------------------------------

Ran 6 tests in 0.001s

OK (skipped=2)

Задачи

Написать тесты ко всем классам из PythonOOPTask1

-- TatyanaDerbysheva - 12 Nov 2017