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.
# Let's create a function that takes every argument
def foo(*args, **kwargs):
return (args, kwargs)
foo('a', 'b')
foo('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