Pytest – Testing Axial-Style Part 3

Selenium and the Page Object Model serve as building blocks for our testing suite at Axial. But we’re missing the glue that connects it all together: pytest. Pytest is a popular alternative to python’s built in unittest framework offering many highly useful features unavailable in unittest. Our favorite features include:

1, Test collection and execution

2, Fixtures and hooks (setup/teardown on steroids)

3, Reporting

To demonstrate pytest we’ll take a break from selenium and focus on sharing resources among test cases. In this demo we’ll write a pytest plugin for a hypothetical testing suite where the majority of test cases connect to our AMS (Axial Messaging Service) API. Wouldn’t it be nice if all our tests made use of the same session key and API connection? Not always, but for this demo the answer is yes.

Pytest Hooks

Our first step is to get an Axial session key that can be shared among all tests. Our plugin can do this by specifying a pytest_sessionstart hook, which is a function that will be called at the beginning of the testing session (i.e. when py.test is invoked). A full list of pytest hooks can be found here. Our sessionstart hook will generate a UUID and add it as a property to pytest’s session object.

from uuid import uuid4

def pytest_sessionstart(session):
session.key = uuid4()

From here on any tests that make use of pytest’s request fixture can access our session key using request.session.key. More on fixtures in the next section.

Pytest Fixtures

At this point all tests can access the same session key but we still want a convenient way for each test to get connection to the AMS API. To solve this problem we will define a custom fixture. As the pytest documentation explains, the purpose of a fixture is to “provide a fixed baseline upon which tests can reliably and repeatedly execute”. Lets dive right in and define a fixture that provides an API connection to be shared among all test cases:

@pytest.fixture(scope="session")

def ams_api(request):
''' Return a connection to the AMS API '''
return AxlClientApiManager(
AMS_HOST,
AMS_PORT,
AMS_API_PATH,
session_key=request.session.key
).api

The decorator tells pytest this is a fixture and the scope=”session” kwargs means the returned connection will be shared among all tests. The request argument is an optional instance of the FixtureRequest object, which tells us everything there is to know about the requesting test case, including the session key we set in our sessionstart hook. In fact, the request instance here is itself a fixture, made available using function arguments. The process of exposing fixtures using function arguments is a prime example of dependency injection. Our new fixture can be used much like the request fixture: as a function argument. For example, to test the echo function of the AMS API all we need to do is:
def test_echo(ams_api):

ams_api.echo('Hello world!')

 

Pytest Plugins

In order to make pytest aware of these fixtures and hooks we need to define a plugin. There are many ways to define a plugin but for our little test suite the most convenient option is to create a conftest.py file that contains our fixtures/hooks and place it in the directory that contains our tests. As a result, any tests run within this directory or any subdirectories will be able to oh-so-magically access the same session key and API connection as described above.

Pytest Collection and Execution

The last step is actually running our test suite. The most common way to execute tests is with the py.test command. Tests are collected by recursively searching the specified directory for modules that begin with test_*, classes that begin with Test* and functions that begin with test_*. Use the –collectonly argument to see which tests will be collected without actually running them.

A rich set of arguments gives control over test execution and configuration. My py.test invocations usually contain the following options. Note that I can define a pytest.ini file with these options in the same directory as conftest.py and they will be used as default options.

-v

Show more when reporting, looks much nicer

--tb-short

Show shorter tracebacks

--capture=no

Don’t capture test stdout. My tests rarely write to stdout and when they do I want to see it right away

--pdb

Open an interactive pdb session on when an unhandled

In part 4 we’ll see how Selenium, the Page Object Model and pytest can be combined to yield a stupid simple web testing framework.