Optional parameters
Note
This tutorial build on the concepts from the Python snippets tutorial.
Many tests have parameters that don’t really need to be specified for every case, e.g. parameters with reasonable defaults. Specifying such parameters over and over again is not only tedious, but also errorprone. Fortunately, there are two good ways to avoid doing this, described below.
To expand on the vector example from the Getting started tutorial, let’s
consider testing a from_angle()
function that initializes a vector from a
given angle (relative to the xaxis). This function will have two optional
parameters:
unit: whether the given angle is in radians or degrees
magnitude: the length of the resulting vector
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def from_angle(angle, *, unit='rad', magnitude=1):
if unit == 'deg':
angle = math.radians(angle)
x = magnitude * math.cos(angle)
y = magnitude * math.sin(angle)
return Vector(x, y)
Schema approach
The schema argument to parametrize
can be used to fill in unspecified
parameters with default values. This takes advantage of the fact that,
although every set of parameters needs to have all the same keys, the schema is
applied before this check is made. So it’s possible for the schema to fill in
any missing keys. In fact, Parametrize From File comes with a defaults
function that does exactly this. The following example shows how it works.
First, the parameter file:
test_from_angle:

angle: 0
expected: Vector(1, 0)

angle: 45
expected: Vector(1/sqrt(2), 1/sqrt(2))

angle: 90
expected: Vector(0, 1)

angle: pi / 2
unit: rad
expected: Vector(0, 1)

angle: 0
magnitude: 2
expected: Vector(2, 0)
Note that unit and magnitude are only specified for one test each. The following schema takes care of evaluating the snippets of python code and filling in the missing defaults:
import vector
import parametrize_from_file as pff
from pytest import approx
with_math = pff.Namespace('from math import *')
with_vec = pff.Namespace(with_math, 'from vector import *')
@pff.parametrize(
schema=[
pff.cast(
angle=with_math.eval,
magnitude=with_math.eval,
expected=with_vec.eval,
),
pff.defaults(
unit='deg',
magnitude=1,
),
],
)
def test_from_angle(angle, unit, magnitude, expected):
actual = vector.from_angle(angle, unit=unit, magnitude=magnitude)
assert actual.x == approx(expected.x)
assert actual.y == approx(expected.y)
It’s significant that the defaults are specified after the cast functions. If they were specified before, they would be processed by the cast functions. In this case, that means they would need to be strings containing python code. Sometimes that’s what you want, but not here.
Note that the test function uses degrees as the default unit, while the function itself uses radians. This is both a good thing and a bad thing. It’s good that our tests will be robust against changes to the default unit. But it’s bad that we’re not actually testing the default unit. If we would like to test this default, we can either (i) write another test specifically for that or (ii) use the dict/list approach described in the next section.
Dict/list approach
For functions that take a lot of arguments, it’s sometimes simpler to define one parameter that contains a variable number of arguments (e.g. akin to args or kwargs) than it is to explicitly specify default values for every optional parameter. This approach is often combined with the schema approach described in the previous section, such that an empty container is assumed if the toplevel parameter isn’t specified:
test_from_angle:

angle: 0
expected: Vector(1, 0)

angle: pi / 4
expected: Vector(1 / sqrt(2), 1 / sqrt(2))

angle: pi / 2
expected: Vector(0, 1)

angle: 90
kwargs:
unit: 'deg'
expected: Vector(0, 1)

angle: 0
kwargs:
magnitude: 2
expected: Vector(2, 0)
One nice feature of Namespace.eval
(see below) is that it recursively handles
dictionaries and lists, which allows use to specify kwargs using either
python or NestedText syntax. Note that this requires us to quote the unit
parameter in the NestedText file, though.
import vector
import parametrize_from_file as pff
from pytest import approx
with_math = pff.Namespace('from math import *')
with_vec = pff.Namespace(with_math, 'from vector import *')
@pff.parametrize(
schema=[
pff.cast(
angle=with_math.eval,
kwargs=with_math.eval,
expected=with_vec.eval,
),
pff.defaults(
kwargs={},
),
],
)
def test_from_angle(angle, kwargs, expected):
actual = vector.from_angle(angle, **kwargs)
assert actual.x == approx(expected.x)
assert actual.y == approx(expected.y)
It’s a little dangerous to set the default kwargs value to a mutable object
like an empty dictionary. Any changes made to this dictionary will persist
between tests, possibly leading to confusing results. You can avoid this issue
by setting the default to None
and replacing it with the desired value within
the test.