Предистория
Однажды на проекте, написанном на 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. Написал пост в надежде, что кому-то поможет это решение, и он не потратит уйму времени и нервов на подобную задачу.
Автор: monkeydo