Arsenic with pytest

A common usage of webdrivers is for testing web applications. Thanks to the async nature of arsenic, you can test your async web applications from the same process and thread as you run your application.

In this guide, we will have a small aiohttp based web application and test it using pytest and pytest-asyncio.

Prerequisites

This guide assumes you are familiar with pytest and it’s terminology.

Setup

You should already have Firefox and Geckodriver installed. Make sure your geckodriver is in your PATH.

Create a virtualenv and install the required dependencies:

python3.6 -m venv env env/bin/pip install –upgrade pip env/bin/pip install –pre arsenic env/bin/pip install pytest-asyncio

App

Our app will have a single handler:

async def index(request):
    data = await request.post()
    name = data.get('name', 'World')
    return Response(status=200, content_type='text/html', body=f'''<html>
    <body>
        <h1>Hello {name}</h1>
        <form method='post' action='/'>
            <input name='name' />
            <input type='submit' />
        </form>
    </body>
</html>''')


def build_app():
    app = Application()
    app.router.add_route('*', '/', index)
    return app

Fixture

To make our app easily available in tests, we’ll write a pytest fixture which runs our app and provides the base url to it, since we will run it on a random port:

@pytest.fixture
async def app(event_loop):
    application = build_app()
    server = await event_loop.create_server(
        application.make_handler(),
        '127.0.0.1',
        0
    )
    try:
        for socket in server.sockets:
            host, port = socket.getsockname()
        yield f'http://{host}:{port}'
    finally:
        server.close()

Arsenic Fixture

We will also write a fixture for arsenic, which depends on the app fixture and provides a running Firefox session bound to the app:

@pytest.fixture
async def session(app):
    session = await start_session(
        services.Geckodriver(),
        browsers.Firefox(),
        bind=app
    )
    try:
        yield session
    finally:
        await stop_session(session)

Test

We will add a simple test which shows that the title on a GET request is Hello World, and if we submit the form it will become Hello followed by what we put into the text field:

async def test_index(session):
    await session.get('/')
    title = await session.wait_for_element(5, 'h1')
    text = await title.get_text()
    assert text == 'Hello World'
    form_field = await session.get_element('input[name="name"]')
    await form_field.send_keys('test')
    submit = await session.get_element('input[type="submit"]')
    await submit.click()
    title = await session.wait_for_element(5, 'h1')
    text = await title.get_text()
    assert text == 'Hello test'

Putting it all together

For this all to work, we’ll need a few imports:

import pytest

from aiohttp.web import Application, Response

from arsenic import start_session, services, browsers, stop_session

And we also need to mark the file as asyncio for pytest to support async functions:

pytestmark = pytest.mark.asyncio

Now to put it all together, create a file called test_pytest.py and insert the following code:

import pytest

from aiohttp.web import Application, Response

from arsenic import start_session, services, browsers, stop_session

pytestmark = pytest.mark.asyncio


async def index(request):
    data = await request.post()
    name = data.get('name', 'World')
    return Response(status=200, content_type='text/html', body=f'''<html>
    <body>
        <h1>Hello {name}</h1>
        <form method='post' action='/'>
            <input name='name' />
            <input type='submit' />
        </form>
    </body>
</html>''')


def build_app():
    app = Application()
    app.router.add_route('*', '/', index)
    return app


@pytest.fixture
async def app(event_loop):
    application = build_app()
    server = await event_loop.create_server(
        application.make_handler(),
        '127.0.0.1',
        0
    )
    try:
        for socket in server.sockets:
            host, port = socket.getsockname()
        yield f'http://{host}:{port}'
    finally:
        server.close()


@pytest.fixture
async def session(app):
    session = await start_session(
        services.Geckodriver(),
        browsers.Firefox(),
        bind=app
    )
    try:
        yield session
    finally:
        await stop_session(session)


async def test_index(session):
    await session.get('/')
    title = await session.wait_for_element(5, 'h1')
    text = await title.get_text()
    assert text == 'Hello World'
    form_field = await session.get_element('input[name="name"]')
    await form_field.send_keys('test')
    submit = await session.get_element('input[type="submit"]')
    await submit.click()
    title = await session.wait_for_element(5, 'h1')
    text = await title.get_text()
    assert text == 'Hello test'

To run it, simply execute pytest test_pytest.py.