RU | EN

От 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('testrn', 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('xd4xc5xd3xd4testrn', 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 = 'xd4xc5xd3xd4Dummy_abcdern' 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

Источники:
gorod-omsk.ru/blog/pythy/

Комментарии: