django

Как я мучал Selenium тесты для GAE Django и к чему пришел в итоге

  • вторник, 25 ноября 2014 г. в 02:10:33
http://habrahabr.ru/post/244009/

Предистория


Однажды на проекте, написанном на GAE Django, понадобилось реализовать тестирование с помощью Selenium. К сожалению, найти готовый инструмента для этого не удалось. Поиски по просторам интернета не дали положительных результатов.

Поворот не туда


Первое, что пришло в голову — это запускать сервер в setUp с помощью
subprocess.Popen
и завершать его в teardown методе
self.server_process.terminate()

На первый взгляд — рабочее решение. Первый вопрос, который возник:
Как заставить запущенный инстанс сервера и testbed использовать одну базу и сервисы?

При запуске сервера указываем, какую БД использовать:

--use_sqlite --datastore_path=/full/path/to/sqlite_db

Говорим testbed с каким app_id и из какой базой запускается тестовый сервер:

self.testbed.setup_env(app_id='some-app-id')
self.testbed.init_datastore_v3_stub(use_sqlite=True, datastore_file='/full/path/to/sqlite_db')

Кажется, готово, но возникает проблема из-за, так сказать, конфликтов testbed и нашего сервера. Сервер запускает необходимые ему сервисы, и testbed делает то-же самое. В общем, «кто последний тот и папочка». Никак не удавалось получить нормально рабочую базу. В итоге, после подбора порядка инициализации у меня получилось и первый тест прошел успешно. Казалось, победа близка, но меня не переставала покидать мысль: чем дальше я продвигаюсь с этими тестами, тем глубже погружаюсь в пучину «грязных» решений.

Необходимо чистить базу


В teardown() методе вызывался testbed.deactivate() и test_server.terminate().
База не очищалась. Добавление --clear_datastore к команде запуска сервера все ломало во время инициализации. Удаление всех данных из базы и очищение кеша вручную также вызывало конфликты.

db.delete(db.Query())
memcache.flush_all()

Пришлось добавить удаление файла базы os.remove, но и это не решало всех проблем.

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

Все гениальное — просто


Не так уж и просто, но проще, чем описанное выше. Спасибо моему коллеге, который натолкнул на мысль использовать LiveServerTestCase. Это стало спасением.

Базовый класс, который активирует testbed и деактивирует его по окончанию всего тесткейса:

class GAELiveServerTestCase(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        cls.testbed = testbed.Testbed()
        cls.testbed.activate()
        cls.testbed.init_datastore_v3_stub()
        cls.testbed.init_memcache_stub()
        cls.testbed.init_channel_stub()
        cls.testbed.init_urlfetch_stub()
        cls.testbed.init_user_stub()
        super(GAELiveServerTestCase, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        cls.testbed.deactivate()
        super(GAELiveServerTestCase, cls).tearDownClass()


Собственно класс selenium тестов.
class SeleniumTestCase(GAELiveServerTestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()

    def tearDown(self):
        self.driver.close()
        self.driver.quit()
        db.delete(db.Query())
        memcache.flush_all()


И один важный момент.

LiveServerTestCase не запускает инстансов GAE типа _ah. Channels были необходимы. Подсмотрев в одном проекте и покопав gae-шные сорцы, нарисовалась дополнительная вьюха, можно сказать, со скопированной логикой из gae для jsapi и connect, disconnect, poll сигналов:

from google.appengine.tools.devappserver2.channel import _JSAPI_PATH

def channel_stub_view(request, page):
    params = request.REQUEST

    if page == 'jsapi':
        return HttpResponse(content=open(_JSAPI_PATH).read(),
                            content_type='text/javascript')

    elif page == 'dev':
        command = params.get('command', None)
        token = params.get('channel', None)
        if command is None or token is None:
            return HttpResponse(status=400)

        stub = apiproxy_stub_map.apiproxy.GetStub('channel')
        try:
            stub.connect_channel(token)
        except (channel_service_stub.InvalidTokenError,
                channel_service_stub.TokenTimedOutError):
            return HttpResponse(status=401)

        client_id = stub.validate_token_and_extract_client_id(token)
        if command == 'connect':
            return HttpResponse(content='1', content_type='text/plain')
        elif command == 'poll':
            message = stub.pop_first_message(token)
            if message is not None:
                return HttpResponse(content=message, content_type='application/json')
    return HttpResponse()


Сделал доступной только во время тестов:

if settings.TESTING:
    urlpatterns += patterns('',
        url(r'^_ah/channel/(?P<page>.*)$', 'channel_stub_view'),
    )


Оставалась одна проблема, когда должен происходить disconnect (например страница закрылась), testbed не понимал этого. Решил проблему инициализацией channel_stub при каждом открытии страницы:

class SeleniumTestCase(GAELiveServerTestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()

    def tearDown(self):
        self.driver.quit()
        db.delete(db.Query())
        memcache.flush_all()

    def open_url(self, url):
        self.get(urljoin(self.live_server_url, url))
        self.testbed.init_channel_stub()


P.S. Написал пост в надежде, что кому-то поможет это решение, и он не потратит уйму времени и нервов на подобную задачу.