Содержание


Разработка через тестирование: Использование PyUnit(unittest)

Обзор модуля

Весь инструментарий, предоставляемый модулем unittest, можно разбить на четыре группы.

Первая группа – среда исполнения теста (Test fixture). Это вспомогательная группа, задача которой – настройка и последующее обнуление данных, используемых в комплекте тестов (test case).

Вторая и наиболее часто используемая группа – комплект тестов (test case), представляющий собой наименьшую единицу тестирования. Это то, из чего будут собираться будущие тесты.

Третья группа – набор тестов (test suite) – реализуется с помощью класса unittest. Test Suite решает задачи упорядочивания и управления комплектами тестов, равно как и наборами тестов. Часто она представляет собой список наборов тестов, исполняемых совместно в течение одной сессии.

Четвертая и последняя группа – группа запуска тестов (test runner). Сюда попали все классы, связанные с запуском и представлением тестов. Группа в состоянии запускать тесты в графическом или текстовом режиме или просто возвращать результат работы для дальнейшей обработки.

Комплект тестов

Начнем со второй группы. Напишем простой тест.

Я не буду долго мудрствовать, изобретая наглядный, удобный и понятный пример, а возьму за основу пример из справки к python 2.6. Будем писать тест для одного из стандартных модулей языка random (листинг 1).

Листинг 1. Простейший тест
#!/usr/bin/env python
#-*- coding:cp1251 -*-

import unittest
import random

class TestSequenceFunctions(unittest.TestCase):
    def testshuffle(self):
        '''
        Убедимся в том, что метод shuffle действительно
   перемешивает список
        '''
        self.seq = range(10)
        random.shuffle(self.seq)
        self.assertNotEqual(self.seq, range(10))

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

Результатом работы данного сценария будет следующий текст в стандартном окне вывода (листинг 2).

Листинг 2. Результат работы сценария из листинга 1
.
-----------------------------------------------------------
Ran 1 test in 0.000s

OK

Сейчас я пропущу подробный разбор вывода сценария и общих, почти для всех сценариев, строк (имеется в виду импорт модулей, указание кодировки, комментарии, документирование и условный переход к запускающему методу unittest.main()). Интерпретацией результатов работы сценария займемся дальше по тексту, когда будем разбирать четвертую группу, отвечающую за запуск и выполнение тестов, а вспомогательный код и так достаточно широко освещен в литературе. Займемся непосредственно классом TestSequenceFunctions.

В нашем случае класс содержит только один метод testshuffle.

Важно! Необходимо, чтобы метод начинался со слова «test». В противном случае метод не будет отработан, и среда сообщит, что 0 (ноль) тестов пройдено.

Такая привязка к слову test удобна в том случае, если потребуется временно отключить какой-либо из тестов, не слишком изменяя код сценария. Достаточно добавить знак подчеркивания в названии метода перед словом тест либо любой другой символ, например: _testshuffle.

Тело метода состоит из трех значимых строк.

  1. Создаем список для тестирования. self.seq = range(10)
  2. Перемешиваем список с помощью метода shaffle модуля random. random.shuffle(self.seq)
  3. Сравниваем получившийся неупорядоченный список с вновь созданным упорядоченным списком.

Первые две строки подготавливают среду для выполнения теста. Третья строка, по сути, есть кульминация данного сценария – то, для чего все это писалось. Используя метод, унаследованный от класса TestCase, мы сравниваем два списка, и в случае, если последовательности не равны, тест будет считаться пройденным, иначе будет сообщено об отрицательном результате.

Давайте расширим тест еще одним методом. Напишем тест для метода choice модуля random. Метод choice возвращает случайный элемент из заданной не пустой последовательности. Добавим в созданный ранее класс TestSequenceFunction следующие строки (листинг 3).

Листинг 3. Тест метода choice
def testchoice(self):
    '''
    Метод choice обязан вернуть случайный элемент из
    переданного ему списка
    '''
    self.seq = range(10)
    element = random.choice(self.seq)
    self.assert_(element in self.seq)

Как видите, метод несложный, и по структуре повторяет приведенный выше метод testshaffle. Все те же построение среды для выполнения теста в первых двух строчках и сам тест. Как вы заметили, для проверки вхождения полученного элемента в родительскую для него последовательность использовался метод класса unittest.TestCase, отличный от метода, используемого в предыдущем тесте. На данный момент в классе TestCase реализовано около 20 таких методов.

Внимательный читатель наверняка обратил свое внимание также на код, дублирующийся для обоих методов. Дублирование – это плохо. Давайте избавимся от него и заодно перейдем к следующей главе.

Среда исполнения теста

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

  • Правило первое. Работа теста не должна зависеть от результатов работы других тестов.
  • Правило второе, как следствие правила первого. Тест должен использовать данные, специально для него подготовленные, и никакие другие.

Конечно, каждый раз вручную прописывать инициализацию данных и окружения для каждого метода в отдельности не есть верное решение. Хорошо, если вся инициализация состоит из двух строк, как у нас. А что, если этих строк будет гораздо больше?

В классе unittest.TestCase имеются методы setUp и tearDown, отвечающие за инициализацию и обнуление среды выполнения теста. Посмотрим, как это выглядит на примере (листинг 4).

Листинг 4. Методы setUp и tearDown
#!/usr/bin/env python
#-*- coding:cp1251 -*-

import unittest
import random

class TestSequenceFunctions(unittest.TestCase):
    
    def setUp(self):
        self.seq = range(10)

    def testshuffle(self):
        '''
        Убедимся в том, что метод shuffled действительно 
        перемешивает список
        '''
        random.shuffle(self.seq)
        self.assertNotEqual(self.seq, range(10))

    def testchoice(self):
        '''
        Метод choice обязан вернуть случайный элемент из 
        переданного ему списка
        '''
        element = random.choice(self.seq)
        self.assert_(element in self.seq)

    def tearDown(self):
        del self.seq

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

Сценарий из листинга 4 отличается от предыдущего наличием методов setUp и tearDown. Эти два метода вызываются соответственно перед и после вызова тестирующего метода. Основная задача setUp – инициализация среды работы теста. Задача tearDown противоположная – он убирает мусор, оставшийся после проведения теста.

В нашем случае мы каждый раз создаем последовательность из десяти элементов, а затем уничтожаем ее.

Как видите, мы избавились от дублирования кода, улучшили читаемость, уменьшив и конкретизировав методы и, что немаловажно, автоматически следуем правилам, приведенным выше в этой главе.

Набор тестов

Во втором и третьем разделе мы рассмотрели процесс создания одиночного комплекта тестов. Если бы размер приложений на языке python ограничивался одним классом и парой методов, то на этом можно было бы и закончить. Но часто даже самый маленький сценарий содержит больше, чем один класс, а сколько их в среднем по размерам приложении? Как управлять расплодившимися экземплярами класса unittest.TestCase? Ведь рабочее окружение для конкретной логической группы классов или отдельно взятого класса обычно сильно отличается от рабочей среды для тестирования другого класса. Как следствие, требуются собственные методы setUp и tearDown. Рано или поздно сценарий разрастется до неудобочитаемого месива из экземпляров класса unittest.TestCase.

Или другая ситуация. Случается, что полное тестирование всех классов системы занимает довольно продолжительное время. Это замедляет процесс разработки и очень быстро взвинтит нервы даже самого стойкого программиста. К тому же, как правило, необходимость в постоянном прогоне тестов существует только для текущей разрабатываемой группы классов. Зачем тестировать все, если остальные части приложения не менялись?

Полагаю, что большинство читателей уже поняли, к чему я веду. Это методы логической группировки и управления тестами. Естественно, что такие средства имеются в python и называются они наборами тестов (TestSuit).

На практике наборы тестов реализуются созданием экземпляра класса unittest.TestSuit и добавлением, с помощью методов addTest и addTests, необходимого количества тестов. Добавлять можно как комплекты тестов (TestCase), так и целые наборы (TestSuit), листинг 5.

Листинг 5. Работа с TestSuit
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(testEx1))
suite.addTest(unittest.makeSuite(testEx2))

В первой строке создается экземпляр класса unittest.TestSuite().

Во второй и третьей к нему добавляется комплект тестов testEx1 и testEx2 соответственно.

К набору тестов можно добавлять не только комплекты тестов, но и готовые наборы, подставляя их вместо комплектов тестов. Например:

suite.addTest (suite1),

где suite1 – набор тестов, включающих, например, тесты testEx3, testEx4 и т.д. либо наборы тестов suite2, suite3.

Теперь при запуске теста будут выполнены все тесты, входящие в набор тестов suit. В нашем случае testEx1, testEx2, testEx3 и т.д., в зависимости от количества добавленных тестов.

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

Простейший способ запустить сценарий тестирования – добавить в скрипт следующие строки (листинг 6).

Листинг 6. Автоматическая загрузка и запуск тестов
if __name__==”__main__”:
	unittest.main()

После старта сценария метод main() модуля unittest автоматически обработает все доступные тесты в текущем модуле.

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

Типичный вызов выглядит так:

ИмяПрограммы [опции] [тесты]

Здесь:

ИмяПрограммы – имя файла сценария; 
[опции] – опции, передаваемые программе (см. таблицу 1);
[тесты] – список комплектов и/или наборов тестов для обработки.
Таблица 1. Список ключей unittest.main
КлючОписание
-h --helpРаспечатать справку
-v --verboseПодробный вывод о результатах работы
-q --quietВыводить минимум информации

Предположим, что имеется файл с комплектом тестов testex1.py следующего содержания (листинг 7).

Листинг 7. Типичный шаблон комплекта тестов
import unittest
import ex1.py

class testEx1(unittest.TestCase):
	
	def setUp(self):
		...
		...

	def testex11(self):
		...
		...
		...

	def testex12(self):
		...
		...
		...

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

Результат выполнения команды testex1.py без параметров будет аналогичен вызову unittest.main(), листинг 8.

Листинг 8. Результат работы команды main
..
-----------------------------------------------------------
Ran 1 test in 0.000s

OK

Две точки обозначают количество обработанных тестов. В случае, если какой-либо из тестов не будет пройден, то вместо точки интерпретатор подставит литеру 'F'. В случае, если при выполнении сценария возникнет ошибка или исключение, то вместо точки будет стоять литера 'E', и под разделительной чертой будет сообщение об ошибке.

Если по каким-либо причинам такой вывод результатов работы программы не устраивает, его можно расширить с помощью ключа –v или --verbose (листинг 9).

Листинг 9. Расширенный вывод команды main
#> textex1.py –v

testex11 (__main__.testex1) ... ok
testex11 (__main__.testex1) ... ok

-----------------------------------------------------------
Ran 1 test in 0.000s

OK

unittest.main позволяет запускать как конкретные тесты, так и определенные наборы тестов, а также списки тестов. Встроенного в нее функционала достаточно для решения подавляющего большинства задач. Но возможности по конфигурированию коллекций и наборов тестов, а также их запуска не ограничиваются только ею. В unittest реализован специальный класс TestRunner(), предварительная инициализация которого гораздо сложнее, но и гораздо более гибкая. Формат статьи не позволяет развернуто описать технику работы с ним, но базовое его описание имеется в справке по языку python.

Заключение

В статье были рассмотрены базовые техники составления и выполнения тестов, их группировки и логического разделения на наборы и комплекты. К сожалению, в связи с ограничением объема статьи пришлось опустить описание конкретных техник и синтаксиса утверждений (assertsion), но они присутствуют в файле справки по языку Python (на данный момент справка доступна только на английском языке).


Ресурсы для скачивания

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=492962
ArticleTitle=Разработка через тестирование: Использование PyUnit(unittest)
publish-date=05272010