How I tormented Selenium tests for GAE Django and what I ended up with

Prehistory


Once on a project written in GAE Django, I needed to implement testing with Selenium. Unfortunately, we could not find a ready-made tool for this. Searches on the Internet did not yield positive results.

Wrong turn


The first thing that came to mind was to start the server in setUp with
subprocess.Popen
and complete it in the teardown method
self.server_process.terminate()

At first glance - a working solution. The first question that arose:
How to make a running server instance and testbed use the same database and services?

When starting the server, indicate which database to use:

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

We tell testbed with which app_id and from which base the test server is launched:

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')

It seems ready, but the problem arises due to, so to speak, conflicts between testbed and our server. The server starts the services it needs, and testbed does the same. In general, "who is the last one and daddy." It was not possible to get a normal working base. As a result, after selecting the initialization order, I succeeded and the first test was successful. The victory seemed to be near, but the thought never stopped leaving me: the further I advance with these tests, the deeper I plunge into the abyss of “dirty” decisions.

It is necessary to clean the base


The teardown () method called testbed.deactivate () and test_server.terminate ().
The base was not cleared. Adding --clear_datastore to the server start command broke everything during initialization. Removing all data from the database and clearing the cache manually also caused conflicts.

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

I had to add the removal of the os.remove database file, but this did not solve all the problems.

With each solution to one problem, another arose. Hands fell, it became worse, from what I write. Went to sleep.

Everything ingenious is simple


Not so simple, but simpler than described above. Thanks to my colleague who came up with the idea of ​​using LiveServerTestCase . It became a salvation.

The base class that activates testbed and deactivates it at the end of the entire test case:

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()


Actually class selenium tests.
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()


And one important point.

LiveServerTestCase does not start GAE instances of type _ah. Channels were needed. Having looked in one project and dug gae-shshy sorts, an additional view was drawn, we can say, with copied logic from gae for jsapi and connect, disconnect, poll signals:

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()


Made available only during tests:

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


There was only one problem when disconnect should occur (for example, the page was closed), testbed did not understand this. Solved the problem by initializing channel_stub each time the page was opened:

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()


PS I wrote a post in the hope that this solution will help someone, and he will not spend a lot of time and nerves on a similar task.

Also popular now: