Эта статья будет посвящена работе с тестированием (Quality Assurance - QA) в Python. Как я рассчитываю, сегодняшний пост один из первых в цикле по работе с QA в Python. Планирую в цикле затронуть разные unit test framework'и (unittest, nose), а также рассмотреть некоторые интересные библиотеки (proboscis). Мы узнаем основной функционал этих библиотек, копнем их поглубже, рассмотрим вопросы тестирования сложных систем с наличием зависимостей между тестами (зло? возможно, но в некоторых случаях оно неизбежно), напишем свой фреймворк поверх unittest для реализации более продвинутой системы отчета, а также других фич: настройка порядка выполнения тестов и возможность задачи зависимостей между ними (test order and test dependency).
Сегодня рассмотрим работу с основой юнит тестирования в Python'е - модулем unittest. Работать будем с Python 2.7.
Рассмотрим простой пример:
import unittest
class BaseTestClass(unittest.TestCase):
def test_add(self):
self.assertEquals(120, 100 + 20)
self.assertFalse(10 > 20)
self.assertGreater(120, 100)
def test_sub(self):
self.assertEquals(100, 140 - 40)
if __name__ == '__main__':
unittest.main()
Запуск файла:
D:\Projects\PyUnitTesting\pyunittest>pyexample_1.py -v
test_add (__main__.BaseTestClass) ... ok
test_sub (__main__.BaseTestClass) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.001s
Перед нами:
import unittest
– подключение unittest модуляclass BaseTestClass(unittest.TestCase)
- объявление Test Class'а. Для распознавания функций в классе и интерпретации их как тесты, необходимо чтобы класс наследовался от unittest.TestCase и тесты в классе должны начинаться с префикса test
def test_add(self):
и def test_sub(self):
– определения тестовunittest.main()
- запуск всех тестов в текущем модуле.Почему
main()
? Копнем в исходники: под вызовомmain()
мы вызываем главный классunittest
модуляTestProgram()
, который в свою очередь прописывает дефолтные инстансы классов загрузки (testLoader
) и прогона (testRunner
) тестов. Ну а после идет вызовTestProgram.runTests()
.Вот как выглядит определение
main
в файлеunittest.main.py::233
:
main = TestProgram
Рассмотрим теперь пример посложнее:
import unittest
class FirstTestClass(unittest.TestCase):
def test_add(self):
self.assertEquals(120, 100 + 20)
class SecondTestClass(unittest.TestCase):
def test_sub(self):
self.val = 210
self.assertEquals(210, self.val)
self.val = self.val - 40
self.assertEquals(170, self.val)
def test_mul(self):
self.val = 210
self.assertEquals(420, self.val * 2)
if __name__ == '__main__':
unittest.main()
Запуск файла:
D:\Projects\PyUnitTesting\pyunittest>pyexample_2.py -v
test_add (__main__.FirstTestClass) ... ok
test_mul (__main__.SecondTestClass) ... ok
test_sub (__main__.SecondTestClass) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.002s
Имеем два обычных класса. Тесты запускаются как обычно.
Однако, в классе SecondTestClass
в начале каждого теста заново инициализируется переменная val
. Нехорошая ситуация, которая решается просто: добавлением метода setUp
в тело класса. Данный метод выполняется перед каждым тестом (Test Case) в наборе (Test Suite):
class SecondTestClass(unittest.TestCase):
def setUp(self):
self.val = 210
def test_sub(self):
self.val = 210
self.assertEquals(210, self.val)
self.val = self.val - 40
self.assertEquals(170, self.val)
def test_mul(self):
self.val = 210
self.assertEquals(420, self.val * 2)
Как видно, писать простые тесты просто. Но что если перед каждым тестом в общем тест-плане надо выполнить предварительную инициализацию какого-либо ресурса либо создать экземпляр другого класса (или, к примеру, открыть файл)? А раз создали в начале, то, вестимо, надо и удалить в конце. Как быть тогда?
Для этого существует ряд методов, уже реализованных в unittest модуле:
setUp
– подготовка прогона теста; вызывается перед каждым тестом.tearDown
– вызывается после того, как тест был запущен и результат записан. Метод запускается даже в случае исключения (exception) в теле теста.setUpClass
– метод вызывается перед запуском всех тестов класса.tearDownClass
– вызывается после прогона всех тестов класса.setUpModule
– вызывается перед запуском всех классов модуля.tearDownModule
– вызывается после прогона всех тестов модуля.setUpClass
и tearDownClass
требуют применения вкупе со @staticmethod
- декоратором, позволяющим объявить функцию внутри класса таким образом, что ей доступ к классу в котором она находится не нужен. Кроме того, данную функцию можно вызвать используя класс (Class.f()
) или его экземпляр (Class().f()
).
setUpModule
и tearDownModule
- реализуются в виде отдельных функций в модуле и не входят ни в один класс модуля.
Пример:
import unittest
def setUpModule():
print "In setUpModule()"
def tearDownModule():
print "In tearDownModule()"
class FirstTestClass(unittest.TestCase):
'''
Invoked without setUp and tearDown methods
'''
def test_add(self):
print "In test_add()"
self.assertEquals(120, 100 + 20)
class SecondTestClass(unittest.TestCase):
'''
Invoked with setUpClass/setUp and tearDown/tearDownClass methods
'''
@staticmethod
def setUpClass():
print "In setUpClass()"
def setUp(self):
print "In setUp()"
def test_sub(self):
print "In test_sub()"
self.assertEquals(210, 110 * 2 - 10)
self.assertEquals(170, 140 - (-30))
def test_mul(self):
print "In test_mul()"
self.assertEquals(420, 210 * 2)
self.assertEquals(420, 210 * 2.0000000000000000000001)
def tearDown(self):
print "In tearDown()"
@staticmethod
def tearDownClass():
print "In tearDownClass()"
if __name__ == '__main__':
unittest.main()
Запуск файла:
D:\Projects\PyUnitTesting\pyunittest>pyexample_3.py
In setUpModule()
In test_add()
.In setUpClass()
In setUp()
In test_mul()
In tearDown()
.In setUp()
In test_sub()
In tearDown()
.In tearDownClass()
In tearDownModule()
----------------------------------------------------------------------
Ran 3 tests in 0.002s
Видно, что в самом начале, до методов setUp()
/tearDown()
идут setUpClass()
/tearDownClass()
, а перед ними setUpModule()
/tearDownModule()
.
Теперь о возможных статусах, присваиваемых тестам.
Они могут быть следующих типов (слева - при обычном выводе (default verbosity), справа - при детализированном (verbosity > 1)):
+----+--------------------+
| . | ok |
| F | FAIL |
| E | ERROR |
| x | expected failure |
| s | skipped 'msg' |
| u | unexpected success |
+-------------------------+
Посмотрим наглядно:
import unittest
class BaseTestClass(unittest.TestCase):
def test_ok(self):
self.assertEquals(210, 110 * 2 - 10)
@unittest.skip('not supported')
def test_skip(self):
self.assertEquals(1000, 10 * 10 * 10)
def test_fail(self):
self.assertEquals(420, 210 * 2.1)
def test_error(self):
raise ZeroDivisionError('Error! Division by zero')
@unittest.expectedFailure
def test_expected(self):
raise ZeroDivisionError('Error! Division by zero')
@unittest.expectedFailure
def test_unexpected_ok(self):
self.assertEquals(1, 1)
if __name__ == '__main__':
unittest.main()
Запуск файла:
D:\Projects\PyUnitTesting\pyunittest>pyexample_4_status.py
ExF.su
....
----------------------------------------------------------------------
Ran 6 tests in 0.001s
FAILED (failures=1, errors=1, skipped=1, expected failures=1, unexpected successes=1)
От теста можно ожидать исключения (exception). Описывается это поведение декоратором @unittest.expectedFailure
. Например, тест рассмотренный в pyexample_4_status.py:
def test_div(self):
raise ZeroDivisionError('Error! Division by zero')
вызывает ERROR (E) с запуском по-умолчанию, но с декоратором @unittest.expectedFailure
:
@unittest.expectedFailure
def test_div(self):
raise ZeroDivisionError('Error! Division by zero')
у нас уже другой статус - expected failure (x).
Если же тест, помеченный этим декоратором, исключения не кидает и пройден успешно, ему присваивается статус - unexpected success (u).
Также тест можно пропустить. Делается это с использованием декоратора @skip
(читайте о разных типах этого декоратора на официальном сайте).
Представьте, что вдруг мы захотели получить такое поведение, при котором ряд классов должны в начале своей работы прогонять общий набор тестов (Test Suite). При этом писать дублирующий код не хочется. Как быть в таком случае? Ответ - наследование:
import unittest
class BaseTestClass(unittest.TestCase):
def test_base_1(self):
self.assertEquals(210, 110 * 2 - 10)
def test_base_2(self):
self.assertTrue(False is not None)
class DerivedTestClassA(BaseTestClass):
def test_derived_a(self):
self.assertEquals(100, 10 * 10)
class DerivedTestClassB(BaseTestClass):
def test_derived_b(self):
self.assertEquals(45, 46 - 1)
if __name__ == '__main__':
unittest.main()
Запуск файла:
D:\Projects\PyUnitTesting\pyunittest>pyexample_5_inheritance.py -v
test_base_1 (__main__.BaseTestClass) ... ok
test_base_2 (__main__.BaseTestClass) ... ok
test_base_1 (__main__.DerivedTestClassA) ... ok
test_base_2 (__main__.DerivedTestClassA) ... ok
test_derived_a (__main__.DerivedTestClassA) ... ok
test_base_1 (__main__.DerivedTestClassB) ... ok
test_base_2 (__main__.DerivedTestClassB) ... ok
test_derived_b (__main__.DerivedTestClassB) ... ok
----------------------------------------------------------------------
Ran 8 tests in 0.005s
Не порядок, помимо тестов наших наследуемых классов (Derived), мы также видим и тесты базового класса. Это происходит, потому что родитель (parent) базового класса – unittest.TestCase
.
Чтобы описанной выше ситуации не произошло, зависимость от unittest
модуля надо убрать.
Но как, в таком случае, сделать чтобы Derived классы все-таки грузились автоматически? Ответ – через множественное наследование (multiple inheritance):
import unittest
class BaseTestClass(object):
def test_base_1(self):
self.assertEquals(210, 110 * 2 - 10)
def test_base_2(self):
self.assertTrue(False is not None)
class DerivedTestClassA(unittest.TestCase, BaseTestClass):
def test_derived_a(self):
self.assertEquals(100, 10 * 10)
class DerivedTestClassB(unittest.TestCase, BaseTestClass):
def test_derived_b(self):
self.assertEquals(45, 46 - 1)
if __name__ == '__main__':
unittest.main()
Запуск файла:
D:\Projects\PyUnitTesting\pyunittest>pyexample_5_multiple_inher.py -v
test_base_1 (__main__.DerivedTestClassA) ... ok
test_base_2 (__main__.DerivedTestClassA) ... ok
test_derived_a (__main__.DerivedTestClassA) ... ok
test_base_1 (__main__.DerivedTestClassB) ... ok
test_base_2 (__main__.DerivedTestClassB) ... ok
test_derived_b (__main__.DerivedTestClassB) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.002s
Прежде чем мы продолжим, немного об устройстве unittest.
Можно выделить несколько основных функциональных блоков пакета:
TestResult и отнаследованный от него TextTestResult:
хранит результаты выполнения тестов. Для реализации своего генератора отчета, обычная практика - отнаследовать от этого класса.
TextTestRunner:
запускает тесты на прогон и работает с TextTestResult в части касаемо оповещения об успехе/провале прогона.
TestLoader:
класс предназначен для создания коллекций тестов (Test Suite)
Если кратко и по сути,
Можно реализовать свой TestRunner и подключить вместо дефолтного. Единственное условие: ваш класс должен реализовывать метод run().
Также можно отнаследовать от unittest'овского TestResult и вызвав родительский __init__()
по другому отслеживать выполнение тестов (переопределение методов addSuccess
, startTest
, прочее).
Более подробно о создании своего TestRunner в одной из следующих статей.
Ладно, глянули немного архитектуру, пора назад - к тестам.
Мы уже знаем как работать с тестами находящимися в одном файле - модуле. Но обычно тестовая система состоит из огромного количества файлов, содержащих не меньшее количество тестов. Как быть?
Рассмотрим синтетический пример:
import unittest
class ClassA(unittest.TestCase):
def test_add_a(self):
self.assertEquals(120, 100 + 20)
def test_sub_a(self):
self.assertEquals(210, 230 - 20)
def test_mul_a(self):
self.assertEquals(420, 105 * 4)
if __name__ == '__main__':
unittest.main()
import unittest
class ClassB(unittest.TestCase):
def test_add_b(self):
self.assertEquals(120, 100 + 20)
def test_sub_b(self):
self.assertEquals(210, 230 - 20)
def test_mul_b(self):
self.assertEquals(420, 105 * 4)
class ClassC(unittest.TestCase):
def test_add_c(self):
self.assertEquals(120, 100 + 20)
def test_sub_c(self):
self.assertEquals(210, 230 - 20)
def test_mul_c(self):
self.assertEquals(420, 105 * 4)
if __name__ == '__main__':
unittest.main()
Оба модуля находятся в одной директории.
Для прогона тестов из обоих файлов, придется вручную создать набор тестов (Test Suite) и внести в него тесты из классов. Для парсинга и занесения тестов с разных модулей мы заюзаем класс Testloader()
, имеющий несколько полезных методов:
Для загрузки тестов из наших двух файлов, мы используем первый метод:
import unittest
import pyexample_6_module_a
import pyexample_6_module_b
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(pyexample_6_module_a)
suite.addTests(loader.loadTestsFromModule(pyexample_6_module_b))
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
Вначале мы импортируем наши модули, затем создаем экземпляр unittest.TestLoader()
и грузим с помощью метода loadTestsFromModule()
тесты. В конце создаем раннер для тестов.
Для пояснения последней и самой важной строчки кода, приведу наглядную иллюстрацию с voidspace.org.uk, статья Introduction to unittest.
loadTestsFromModule()
Этот метод мы уже использовали в pyexample_6.py. Он грузит все тесты со всех классов указанного модуля (в нашем случае классы ClassA, ClassB и ClassC с двух файлов-модулей):
suite = loader.loadTestsFromModule(pyexample_6_module_a)
suite.addTests(loader.loadTestsFromModule(pyexample_6_module_b))
Результат:
D:\Projects\PyUnitTesting\pyunittest>pyexample_6.py
test_add_a (pyexample_6_module_a.ClassA) ... ok
test_mul_a (pyexample_6_module_a.ClassA) ... ok
test_sub_a (pyexample_6_module_a.ClassA) ... ok
test_add_b (pyexample_6_module_b.ClassB) ... ok
test_mul_b (pyexample_6_module_b.ClassB) ... ok
test_sub_b (pyexample_6_module_b.ClassB) ... ok
test_add_c (pyexample_6_module_b.ClassC) ... ok
test_mul_c (pyexample_6_module_b.ClassC) ... ok
test_sub_c (pyexample_6_module_b.ClassC) ... ok
----------------------------------------------------------------------
Ran 9 tests in 0.004s
loadTestsFromName()
& loadTestsFromNames()
loadTestsFromName
принимает строку содержащую:
suite.addTests(loader.loadTestsFromName(‘pyexample_6_module_b’))
Результат:
D:\Projects\PyUnitTesting\pyunittest>pyexample_6.py
test_add_b (pyexample_6_module_b.ClassB) ... ok
test_mul_b (pyexample_6_module_b.ClassB) ... ok
test_sub_b (pyexample_6_module_b.ClassB) ... ok
test_add_c (pyexample_6_module_b.ClassC) ... ok
test_mul_c (pyexample_6_module_b.ClassC) ... ok
test_sub_c (pyexample_6_module_b.ClassC) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.004s
suite.addTests(loader.loadTestsFromName(‘pyexample_6_module_b.ClassB’))
Результат:
D:\Projects\PyUnitTesting\pyunittest>pyexample_6.py
test_add_b (pyexample_6_module_b.ClassB) ... ok
test_mul_b (pyexample_6_module_b.ClassB) ... ok
test_sub_b (pyexample_6_module_b.ClassB) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.002s
suite.addTests(loader.loadTestsFromName(‘pyexample_6_module_b.ClassC.test_add_c’))
Результат:
D:\Projects\PyUnitTesting\pyunittest>pyexample_6.py
test_add_c (pyexample_6_module_b.ClassC) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
loadTestsFromNames
от loadTestsFromName
отличается только тем, что принимает на вход последовательность строк:
suite.addTests(loader.loadTestsFromNames(
[‘pyexample_6_module_b.ClassB’, ‘pyexample_6_module_b.ClassC.test_add_c’]))
Результат:
D:\Projects\PyUnitTesting\pyunittest>pyexample_6.py
test_add_b (pyexample_6_module_b.ClassB) ... ok
test_mul_b (pyexample_6_module_b.ClassB) ... ok
test_sub_b (pyexample_6_module_b.ClassB) ... ok
test_add_c (pyexample_6_module_b.ClassC) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.003s
loadTestsFromTestCase()
Этот метод лишь грузит набор тестов (Test Case) из указанного класса (Test Suite). Пусть вас не смущает то что метод имеет в свое названии сочетание TestCase (тест), на деле он грузит не единичный тест, а все тесты с класса. В данном случае Test Case обозначает именно класс, наследуемый от unittest.TestCase()
.
Замечание: разные assert функции описывать я не буду - по ним море информации и в официальных источниках и в разных блогах.
Замечание: для примеров ниже, предполагается, что все рассмотренные файлы выше находятся в одной дректории.
Допустим, все тесты написаны. Как прогнать их все разом, если они не находятся в одном файле/модуле? Вообще, тут два варианта. Для обоих необходимо создать некий стартовый (bootstrap) файл, запускающий весь процесс прогона.
TestLoader.discover()
- данная фича появилась в unittest совсем недавно:import unittest
loader = unittest.TestLoader()
suite = loader.discover(start_dir='.', pattern='pyexample_*.py')
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
Для того, что авто discovery работал, необходимо, чтобы все файлы, содержащие тест кейсы представляли собой модули (modules) или пакеты (packages) в питоновском понимании.
pip install discover
python -m discover -p “pyexample_*.py”
-p "pyexample_*.py"
в данном случае лишь смена шаблона по которому ищатся файлы с тестами.
В результате выполнения как первого варианта, так и второго, в исполнение запустятся все тесты всех классов всех модулей.
На этом пока все. В следующей статье цикла - создание своего TestRunner‘а.
« Previous Blog Post | Back to top | Next Blog Post »