Siv Scripts

Solving Problems Using Code

Wed 03 January 2018

Adding Function Arguments to pytest Fixtures

Posted by Aly Sivji in Quick Hits   

Testing is one of my favourite things about programming. There is nothing like the piece of mind that comes from being able to modify code without having to worry about breaking something.

Like most Python programmers, I started testing using the PyUnit framework (aka unittest) included in Python's Standard Library. PyUnit is part of the xUnit family of testing frameworks. There are lots of great resources on how to test using xUnit which is great!

But there is also the not great: xUnit has its roots in Java so PyUnit is verbose. Tests need to be grouped into classes, special assert* methods are required for verification, and the object-oriented design of PyUnit results in over-engineered tests.

This is where pytest shines. pytest gets out of our way so we can spend our time writing tests versus boilerplate code.

The benfits of pytest are most clearly seen in the implementation of fixtures. Test fixtures allow us to setup assumptions for our system under test (SUT); this makes it possible to create reproducible results.

Some examples of test fixtures include:

  • setting up a database to a preconfigured state
  • cleaning up a database after tests are run
  • capturing logging output
  • loading test data from a JSON file; great for testing webhooks!
  • initializing test objects

In pytest, we use the @pytest.fixture decorator to create fixtures. pytest will then insert fixtures into our test function via dependency injection.

Below we see a simple example:

# simple pytest fixture
@pytest.fixture()
def client():
    return testing.TestClient(api)

# -----------------------------------------

# injecting fixture into test method
def test_get_successful_response(client, mocker):
    # Arrange
    mock_datetime = mocker.patch.object(backend.cta, 'datetime')
    mock_datetime.datetime.now.return_value = (
        datetime.datetime(2017, 11, 14, 15, 56)
    )

    # Act
    response = client.simulate_get('/stops/1066')

    # Assert
    upcoming_buses = response.json['result']
    assert response.status == falcon.HTTP_200
    assert len(upcoming_buses) == 4
    assert upcoming_buses[0] == {'bus': '146', 'min_away': 3}
    assert upcoming_buses[1] == {'bus': '151', 'min_away': 10}

Looks fairly Pythonic if I do say so myself!

pytest fixtures are pretty awesome: they improve our tests by making code more modular and more readable. But that's not all! We can leverage the power of first-class functions and make fixtures even more flexible!

In this post we will walkthrough an example of how to create a fixture that takes in function arguments.

As we discussed in previous episodes, Python functions are objects. This means we can:

  • assign functions to variables
  • store them as data structures
  • pass functions as arguments to other functions
  • return functions as values
In [1]:
# Let's create a function that takes every argument
def foo(*args, **kwargs):
    return (args, kwargs)
In [2]:
foo('a', 'b')
Out[2]:
(('a', 'b'), {})
In [3]:
foo('a', b=2, c='test')
Out[3]:
(('a',), {'b': 2, 'c': 'test'})

The results are what we expect. We can read function arguments inside of our function! Exciting stuff!

Now let's create a pytest fixture that replicates the behavior of foo(...). To do this we will need to create a function inside of our fixture function and return it as follows:

# test_example.py (1/2)

import pytest

@pytest.fixture()
def argument_printer():
    def _foo(*args, **kwargs):
        return (args, kwargs)

    return _foo

Now everytime we inject argument_printer into a test method, it will refer to the _foo(...) function. Using this information, we can add arguments to our fixture as follows:

# test_example.py (2/2)

def test_example(argument_printer):
    first_case = argument_printer('a', 'b')
    assert first_case == (('a', 'b'), {})

    second_case = argument_printer('a', b=2, c='test')
    assert second_case == (('a',), {'b': 2, 'c': 'test'})

Running it thru the pytest-runner:

$ pytest
================================= test session starts ==================================
platform darwin -- Python 3.6.2, pytest-3.3.1, py-1.5.2, pluggy-0.6.0
rootdir: /Users/alysivji/Documents/siv-dev/projects/blog-notebooks, inifile:
plugins: mock-1.6.3, ipynb-1.1.0, cov-2.5.1
collected 56 items

quick-hits/playground/025/example_test.py .                                      [100%]

=============================== 1 passed in 0.01 seconds ===============================

Looks good!

A good use case for having fixtures that take arguments is loading test data from a JSON file.

This testing pattern comes in handy when we have to write tests around an API. We can load predefined data from text files and write assertions based on expected output as shown in the following example:

import json
import pytest

@pytest.fixture
def json_loader():
    """Loads data from JSON file"""

    def _loader(filename):
        with open(filename, 'r') as f:
            print(filename)
            data = json.load(f)
        return data

    return _loader


def test_wrong_stop(client, mocker, json_loader):
    # Arrange
    get_mock = mocker.MagicMock()
    get_mock.status_code = 200
    get_mock.json.return_value = json_loader(
        cta_error_incorrect_stop_response.json)
    mocker.patch.object(
        backend.cta.requests,
        'get',
        return_value=get_mock,
    )

    # Act
    response = client.simulate_get('/stops/106')

    # Assert
    assert response.status == falcon.HTTP_200
    assert response.json == {'error': 'stop_id: 106 does not exist'}

🙌 🙌 🙌

Testing is easy once you have the right tools.

Note: this example is adapted from the sivmetric-backend repo on my Github.


 
    
 
 

Comments