0

I am trying to add Selenium tests to a Flask application that relies heavily on Flask-SocketIO. However, when accessing a page from the Selenium browser that does a database query, I get the following error message in the browser window:

Traceback (most recent call last):
  File "/home/bbbart/.virtualenvs/heistmanager/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 346, in connection
    return self.__connection
AttributeError: 'Connection' object has no attribute '_Connection__connection'

During handling of the above exception, another exception occurred:
[...]
 File "/home/bbbart/.virtualenvs/heistmanager/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 429, in _revalidate_connection
    raise exc.ResourceClosedError("This Connection is closed")
sqlalchemy.exc.ResourceClosedError: This Connection is closed
  • bbbart is my username
  • heistmanager is the name of the application
  • I am using the following PyPI package (among others):
    • Flask==0.12.2
    • SQLAlchemy==1.1.13
    • Flask-SQLAlchemy==2.2
    • Flask-SocketIO==2.9.2
    • Flask-Script==2.0.5
    • eventlet==0.21.0
    • pytest==3.2.1
    • pytest-flask==0.10.0
    • pytest-selenium==1.11.0

I have an extensive pytest suite for this application running perfectly already, including tests relying on the database and tests using the client fixture from pytest-flask. It's only when starting the server externally for Selenium that this error appears.

In order for the app to start correctly with support for websockets I have overridden the LiveServer class and the live_server fixture from pytest_flask as follows (is this necessary?) in conftest.py:

class LiveSocketIOServer(LiveServer):
    """The helper class to manage live socketio server. Handles creation and
    stopping of the application in a separate process.

    :param app: The application to run.
    :param port: The port to run application.
    """

    def start(self, socketio):
        """Start application in a separate process."""
        def worker(app, port):
            return socketio.run(app, port=port, use_reloader=False)

        self._process = multiprocessing.Process(
            target=worker,
            args=(self.app, self.port)
        )   
        self._process.start()

        # We must wait for the server to start listening with a maximum
        # timeout of 5 seconds.
        timeout = 5 
        while timeout > 0:
            time.sleep(1)
            try:
                urlopen(self.url())
                timeout = 0 
            except BaseException:
                timeout -= 1

    def __repr__(self):
        return '<LiveSocketIOServer listening at %s>' % self.url()

and

@pytest.yield_fixture(scope='function')
def live_server(request, app, monkeypatch):
    """Run application in a separate process.

    When the ``live_server`` fixture is applied, the ``url_for`` function
    works as expected::

        def test_server_is_up_and_running(live_server):
            index_url = url_for('index', _external=True)
            assert index_url == 'http://localhost:5000/'

            res = urllib2.urlopen(index_url)
            assert res.code == 200

    """
    # Find an open port
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('', 0))
    port = sock.getsockname()[1]
    sock.close()

    server_name = app.config['SERVER_NAME'] or 'localhost'
    monkeypatch.setitem(app.config, 'SERVER_NAME',
                        _rewrite_server_name(server_name, str(port)))

    server = LiveSocketIOServer(app, port)
    if request.config.getvalue('start_live_server'):
        server.start(SOCKETIO)

    yield server

    server.stop()

My test looks like this:

@pytest.mark.usefixtures('live_server')
class TestSockets:

    """Tests for socket.io (with selenium)."""

    @pytest.mark.nondestructive
    def test_admin_login(self, config, selenium):
        selenium.get(url_for('main.index', _external=True))
        assert 'Hello stranger!' in selenium.page_source

        selenium.find_element_by_link_text('Log in to administer').click()
        assert '<h1>Login</h1>' in selenium.page_source

        selenium.find_element_by_name('username').send_keys(
            config['USERNAME_ADMIN'])
        selenium.find_element_by_name('password').send_keys('*******')
        selenium.find_element_by_name('submit').click()
        assert 'Logged in as %s' % config['USERNAME_ADMIN'] in selenium.page_source

(You might recognise some elements from Miguel Grinberg's excellent Flasky demo. :-))

The error above is raised after the second click call in the penultimate line of the test. This subsequently of course makes the assert statement fail, but that's not the point here.

The tests are ran from a flask-script Manager command:

APP = create_app(os.getenv('HEIST_CONFIG') or 'default')
MANAGER = Manager(APP)

@MANAGER.command
def test():
    """Run the tests."""
    import pytest
    pytest.main(['--driver', 'Chrome',
                 os.path.join(basedir, 'tests')])

I spent a few hours now on trying to figure out why the SQLAlchemy connection is closed (or never even opened?). I added all sorts of DB stuff to the fixtures, but that never felt right and also never worked.

I'm lost and would appreciate any pointer here towards a solution. Thanks!

Bart Van Loon
  • 1,430
  • 8
  • 18
  • What database are you using? I believe this error occurs when the app tries to use a db session that the database server considers invalid. For example, restarting the db while the app is running could cause this. – Miguel Grinberg Aug 19 '17 at 17:36
  • I am using SQLite3. For the test cases my `SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'`. So, there is no real database server, but pytest of course opens/closes connections and creates/removes sessions for every test function through a fixture. – Bart Van Loon Aug 19 '17 at 23:10
  • My only suggestion would be to modify the way you start your server so that the app object is created in the child process. In your current solution the app is created in the parent process. My guess is that the database instance suffers somehow from the transfer from the parent to the child process. Alternatively, you can launch the server on a thread within the same process, like [I did](https://github.com/miguelgrinberg/flasky/blob/master/tests/test_selenium.py#L48). – Miguel Grinberg Aug 20 '17 at 00:12
  • Thanks for the suggestion. I still can't get it to work however. So many issues keep popping up. I started summarizing them, but SO is limiting the number of characters in this textarea. :-/ Anyway, the tests run fine without the live_server fixture and a manual run of the server in a separate terminal, I'll do that for now. I also ran into http://docs.sqlalchemy.orgeg/en/latest/dialects/sqlite.html#threading-pooling-behavior along the way. Is there any example online of working selenium tests for a flask-socketio application? I'm kind of baffled I don't seem to be able to find any. – Bart Van Loon Aug 20 '17 at 21:18
  • Are you sure Flask-SocketIO and Selenium have anything to do with the problem? My guess is that it is just the starting of the Flask server as a subprocess, which messes up the database session. When I test with Selenium I run the Flask server in a thread within the same process, never tried starting a subprocess, seems too heavyweight. – Miguel Grinberg Aug 20 '17 at 22:11
  • I thought that was the problem, since using flask-socketio was the only significant difference I saw between my app and the various online demos. However, now I think I must be abusing SQLAlchemy somewhere. I added a `shell` flask_script command and any database related command I issue results in `RuntimeError: application not registered on db instance and no application bound to current context` from flask_sqlalchemy. My `db` object looks like ``. Time for some studying, debugging and refactoring, I'm afraid. – Bart Van Loon Aug 24 '17 at 14:25

0 Answers0