От Perl-скрипта к Twisted-приложению: Пишем юнит-тесты ∞
Сегодня пишем юнит-тесты для нашего Twisted-приложения. У Twisted хорошая традиция полных юнит-тестов. Если исправляется ошибка, то обязательно пишется юнит-тест, который не проходит в оригинале и нормально завершается в исправленном варианте.
Методика экстремального программирования, предлагает писать юнит-тесты до написания кода. Я предпочитаю писать юнит-тесты по завершению функции/класса. Я использую юнит-тесты для контроля правильности реализации интерфейса и отсутствия повтора уже исправленных ошибок. Т.е. у меня на всех этапах (создание "скелета", переход на асинхронную обработку, внедрение unicode) были юнит-тесты, которые контролировали правильность реализации. Естественно, на каждом следующем этапе, предыдущие тесты "ломались". Понятное дело, что изменения не подтверждались (commit) в системе контроля версий (я использую Subversion) до тех пор, пока все тесты корректно не завершались.
Пару слов о юнит-тестах в Twisted. Их обеспечивает пакет twisted.trial. API аналогичен стандартному unittest, правда, добавляется пара-тройка особенностей:
-
метод
tearDown
- метод для корректного завершения набора тестов - в случае, если тест возвращает Deferred, trial дожидается его завершения
Вдобавок, у trial есть договоренность о том, что юнит-тесты пакета package
находится в package.test
. И набор юнит-тестов для данного пакета запускается командой trial package
.
Итак, для последней версии (асинхронная обработка + unicode), я написал такие юнит-тесты для клиента:
from twisted.trial import unittest
from TwistedPythy import clients
class UnicodeDummyClientTestCase(unittest.TestCase):
def setUp(self):
self.client = clients.UnicodeDummyClient()
def test_getDummyClient(self):
self.assertEquals(u'Dummy_10', self.client.getClient(u'10'))
def test_getDummyStrippedClient(self):
# line with length >20 must bet stripped
self.assertEquals(u'Dummy_1234567890abcd', self.client.getClient(u'1234567890abcdefgh'))
def test_getDummyClientNotUnicode(self):
# str instead of unicode raises AssertError
self.assertRaises(AssertionError, self.client.getClient, '10')
Для протокола:
from twisted.trial import unittest
from twisted.internet import protocol, defer
from TwistedPythy import clients, proto
from twisted.test.test_protocols import StringIOWithoutClosing as SIOWOC
class TestAsyncUnicodePythyProto(proto.AsyncUnicodePythyProto):
""" AsyncUnicodePythyProto for unittests"""
def __init__(self):
self.deferred = defer.Deferred()
self.debug = False
def sendAnswer(self, *args, **kwargs):
proto.AsyncUnicodePythyProto.sendAnswer(self, *args, **kwargs)
self.deferred.callback('answer sent')
class AsyncUnicodePythyProtoTestCase(unittest.TestCase):
def setUp(self):
self.p = TestAsyncUnicodePythyProto()
self.c = clients.UnicodeDummyClient()
self.f = proto.AsyncUnicodePythyFactory(self.c, 'koi8-r')
self.p.factory = self.f
def cbCheckReceived(self, res, data):
orig_data, pseudo_handler = data
self.assertEquals(orig_data, pseudo_handler.getvalue())
def test_sendLineUnicode(self):
s = SIOWOC()
self.p.makeConnection(protocol.FileWrapper(s))
self.p.sendLine(u'test')
self.assertEquals('test\r\n', s.getvalue())
def test_sendLineStr(self):
s = SIOWOC()
self.p.makeConnection(protocol.FileWrapper(s))
self.assertRaises(AssertionError, self.p.sendLine, 'test')
def test_sendAnswerUnicode(self):
s = SIOWOC()
self.p.makeConnection(protocol.FileWrapper(s))
self.p.sendAnswer(u'test')
self.assertEquals('\xd4\xc5\xd3\xd4test\r\n', s.getvalue())
def test_sendAnswerStr(self):
s = SIOWOC()
self.p.makeConnection(protocol.FileWrapper(s))
self.assertRaises(AssertionError, self.p.sendAnswer, 'test')
def test_lineReceived(self):
req = '0123456789abcdefghijk'
ans = '\xd4\xc5\xd3\xd4Dummy_abcde\r\n'
s = SIOWOC()
self.p.makeConnection(protocol.FileWrapper(s))
self.p.lineReceived(req)
self.p.deferred.addCallback(self.cbCheckReceived, (ans, s))
return self.p.deferred
Для выполнения юнит-тестов необходимо, чтобы пакет TwistedPythy был в
PYTHONPATH
. Т.е. в переменнуюPYTHONPATH
нужно добавить путь к TwistedPythy. Например, в Linux:
pythy@axcel:~$ export PYTHONPATH=$PYTHONPATH:/home/pythy/blog/twistedpythy_04
в Windows:
C:\> set PYTHONPATH=%PYTHONPATH%;d:\pythy\blog\twistedpythy_04
Ну а результат выполнения всех тестов выглядит так:
pythy@axcel:~$ trial TwistedPythy
Running 21 tests.
TwistedPythy.test.test_clients
DummyClientTestCase
test_getDummyClientStr ... [OK]
test_getDummyClientUnicode ... [OK]
test_getDummyStrippedClient ... [OK]
UnicodeDummyClientTestCase
test_getDummyClientStr ... [OK]
test_getDummyClientUnicode ... [OK]
test_getDummyStrippedClient ... [OK]
TwistedPythy.test.test_proto
AsyncPythyProtoTestCase
test_lineReceived ... [OK]
test_sendAnswerStr ... [OK]
test_sendAnswerUnicode ... [OK]
test_sendLineStr ... [OK]
test_sendLineUnicode ... [OK]
AsyncUnicodePythyProtoTestCase
test_lineReceived ... [OK]
test_sendAnswerStr ... [OK]
test_sendAnswerUnicode ... [OK]
test_sendLineStr ... [OK]
test_sendLineUnicode ... [OK]
PythyProtoTestCase
test_lineReceived ... [OK]
test_sendAnswerStr ... [OK]
test_sendAnswerUnicode ... [OK]
test_sendLineStr ... [OK]
test_sendLineUnicode ... [OK]
-------------------------------------------------------------------------------
Отмечу, что тесты выполняются в алфавитном порядке, т.е. не в том порядке, в каком я их писал. Поэтому, требование изолированности (результат выполнения теста не влияет на другие тесты) вытекает само-собой.
Еще обращу ваше внимание на то, как пишутся тесты для асинхронных событий - если сделать как в сихронном варианте:
self.p.lineReceived(req)
self.assertEquals(ans, s.getvalue())
то такой тест будет проваливаться, в силу того, что ответ "в линию" будет даваться позже чтения из линии. Т.е. s.getvalue()
будет снимать показания до того, как sendAnswer()
пошлет ответ. Чтобы этого не происходило, нужно "дожидаться", пока sendAnswer()
не выполниться и только после этого считывать данные "с линии". Для этого я создал дочерний класс TestAsyncPythyProto
, у которого отдельным атрибутом завел deferred
, выполняемый при посылке ответа. А в тесте просто добавил callback для проверки значений - cbCheckReceived
и возвращаю deferred
.
self.p.lineReceived(req)
self.p.deferred.addcallback(self.cbCheckReceived, (ans, s))
return self.p.deferred
Как я уже выше говорил, trial, получив Deferred-объект от теста, "знает", что с ним делать и вполне корректно обрабатывает тест.
При создании этих тестов основными сложностями было именно Twisted-специфика: как "подключать" протокол к тесту, как работать с Deferred-объектами. Когда уже написал тесты, то понял, что дело не в сложности Twisted, а в малой документированности twisted.trial: в документации Twisted о нем говорится весьма кратко, а в книге Twisted Essentials вообще ничего. Поэтому основным источником был код Twisted и юнит-тесты к нему.
Надеюсь, что данный пост хоть чуть-чуть поможет вам в освоении twisted.trial. Как всегда, код можно взять на RapidShare