Test Fixtures¶
Slash includes a powerful mechanism for parametrizing and composing tests, called fixtures. This feature resembles, and was greatly inspired by, the feature of the same name in py.test.
To demonstrate this feature we will use test functions, but it also applies to test methods just the same.
What is a Fixture?¶
A fixture refers to a certain piece of setup or data that your test requires in order to run. It generally does not refer to the test itself, but the base on which the test builds to carry out its work.
Slash represents fixtures in the form of arguments to your test function, thus denoting that your test function needs this fixture in order to run:
def test_microwave_turns_on(microwave):
microwave.turn_on()
assert microwave.get_state() == STATE_ON
So far so good, but what exactly is microwave? Where does it come from?
The answer is that Slash is responsible of looking up needed fixtures for each test being run. Each function is examined, and telling by its arguments, Slash goes ahead and looks for a fixture definition called microwave.
The Fixture Definition¶
The fixture definition is where the logic of your fixture goes. Let’s write the following somewhere in your file:
import slash
...
@slash.fixture
def microwave():
# initialization of the actual microwave instance
return Microwave(...)
In addition to the test file itself, you can also put your fixtures in a file called slashconf.py, and put it in your test directory. Multiple such files can exist, and a test automatically “inherits” fixtures from the entire directory hierarchy above it.
Fixture Cleanups¶
You can control what happens when the lifetime of your fixture ends. By default, this happens at the end of each test that requested your fixture. To do this, add an argument for your fixture called this
, and call its add_cleanup
method with your cleanup callback:
@slash.fixture
def microwave(this):
returned = Microwave()
this.add_cleanup(returned.turn_off)
return returned
Note
Ths this
variable is also available globally while computing each fixture as the slash.context.fixture
global variable.
Opting Out of Fixtures¶
In some cases you may want to turn off Slash’s automatic deduction of parameters as fixtures. For instance in the following case you want to explicitly call a version of a base class’s before
method:
>>> class BaseTest(slash.Test):
... def before(self, param):
... self._construct_case_with(param)
>>> class DerivedTest(BaseTest):
... @slash.parametrize('x', [1, 2, 3])
... def before(self, x):
... param_value = self._compute_param(x)
... super(DerivedTest, self).before(x)
This case would fail to load, since Slash will assume param
is a fixture name and will not find such a fixture to use. The solution is to use slash.nofixtures()
on the parent class’s before
method to mark that param
is not a fixture name:
>>> class BaseTest(slash.Test):
... @slash.nofixtures
... def before(self, param):
... self._construct_case_with(param)
Fixture Needing Other Fixtures¶
A fixture can depend on other fixtures just like a test depends on the fixture itself, for instance, here is a fixture for a heating plate, which depends on the type of microwave we’re testing:
@slash.fixture
def heating_plate(microwave):
return get_appropriate_heating_plate_for(microwave)
Slash takes care of spanning the fixture dependency graph and filling in the values in the proper order. If a certain fixture is needed in multiple places in a single test execution, it is guaranteed to return the same value:
def test_heating_plate_usage(microwave, heating_plate):
# we can be sure that heating_plate matches the microwave,
# since `microwave` will return the same value for the test
# and for the fixture
Fixture Parametrization¶
Fixtures become interesting when you parametrize them. This enables composing many variants of tests with a very little amount of effort. Let’s say we have many kinds of microwaves, we can easily parametrize the microwave class:
@slash.fixture
@slash.parametrize('microwave_class', [SimpleMicrowave, AdvancedMicrowave]):
def microwave(microwave_class, this):
returned = microwave_class()
this.add_cleanup(returned.turn_off)
return returned
Now that we have a parametrized fixture, Slash takes care of multiplying the test cases that rely on it automatically. The single test we wrote in the beginning will now cause two actual test cases to be loaded and run – one with a simple microwave and one with an advanced microwave.
As you add more parametrizations into dependent fixtures in the dependency graph, the actual number of cases being run eventually multiples in a cartesian manner.
Fixture Requirements¶
It is possible to specify requirements for fixture functions, very much like test requirements. Fixtures for which requirements are not met will prevent their dependent tests from being run, being skipped instead:
@slash.fixture
@slash.requires(condition, 'Requires a specific flag')
def some_fixture():
...
See also
Fixture Scopes¶
By default, a fixture “lives” through only a single test at a time. This means that:
- The fixture function will be called again for each new test needing the fixture
- If any cleanups exist, they will be called at the end of each test needing the fixture.
We say that fixtures, by default, have a scope of a single test, or test scope.
Slash also supports session and module scoped fixtures. Session fixtures live from the moment of their activation until the end of the test session, while module fixtures live until the last test of the module that needed them finished execution. Specifying the scope is rather straightforward:
@slash.fixture(scope='session')
def some_session_fixture(this):
@this.add_cleanup
def cleanup():
print('Hurray! the session has ended')
@slash.fixture(scope='module')
def some_module_fixture(this):
@this.add_cleanup
def cleanup():
print('Hurray! We are finished with this module')
Test Start/End for Widely Scoped Fixtures¶
When a fixture is scoped wider than a single test, it is useful to add custom callbacks to the fixtures to be called when a test starts or ends. This is done via the this.test_start
and this.test_end
callbacks, which are specific to the current fixture.
@slash.fixture(scope='module')
def background_process(this):
process = SomeComplexBackgroundProcess()
@this.test_start
def on_test_start():
process.make_sure_still_running()
@this.test_end
def on_test_end():
process.make_sure_no_errors()
process.start()
this.add_cleanup(process.stop)
Note
Exceptions propagating out of the test_start
or test_end
hooks will fail the test, possibly preventing it from starting properly
Autouse Fixtures¶
You can also “force” a fixture to be used, even if it is not required by any function argument. For instance, this example creates a temporary directory that is deleted at the end of the session:
@slash.fixture(autouse=True, scope='session')
def temp_dir():
"""Create a temporary directory"""
directory = '/some/directory'
os.makedirs(directory)
@this.add_cleanup
def cleanup():
shutil.rmtree(directory)
The use_fixtures Decorator¶
In some cases, you may want to use a certain fixture but don’t need its return value. In such cases, rather than using the fixture as an unused argument to your test function you can use the use_fixtures
decorator. This decorator receives a list of fixture names and indicates that the decorated test needs them to run:
@slash.fixture()
def used_fixture1():
"""do something"""
pass
@slash.fixture()
def used_fixture2():
"""do another thing"""
pass
@slash.use_fixtures(["used_fixture1, used_fixture2"])
def test_something():
pass
Aliasing Fixtures¶
In some cases you may want to name your fixtures descriptively, e.g.:
@slash.fixture
def microwave_with_up_to_date_firmware(microwave):
microwave.update_firmware()
return microwave
Although this is a very nice practice, it makes tests clumsy and verbose:
def test_turning_off(microwave_with_up_to_date_firmware):
microwave_with_up_to_date_firmware.turn_off()
assert microwave_with_up_to_date_firmware.is_off()
microwave_with_up_to_date_firmware.turn_on()
Fortunately, Slash allows you to alias fixtures, using the slash.use()
shortcut:
def test_turning_off(m: slash.use('microwave_with_up_to_date_firmware')):
m.turn_off()
assert m.is_off()
m.turn_on()
Note
Fixture aliases rely on function argument annotation
Misc. Utilities¶
Yielding Fixtures¶
Fixtures defined as generators are automatically detected by Slash. In this mode, the fixture is run as a generator, with the yielded value acting as the fixture value. Code after the yield is treated as cleanup code (similar to using this.add_cleanup
):
@slash.fixture
def microwave(model_name):
m = Microwave(model_name)
yield m
m.turn_off()
Generator Fixtures¶
slash.generator_fixture()
is a shortcut for a fixture returning a single parametrization:
@slash.generator_fixture
def model_types():
for model_config in all_model_configs:
if model_config.supported:
yield model_config.type
In general, this form:
@slash.generator_fixture
def fixture():
yield from x
is equivalent to this form:
@slash.fixture
@slash.parametrize('param', x)
def fixture(param):
return param
Listing Available Fixtures¶
Slash can be invoked with the list
command and the --only-fixtures
flag, which takes a path to a testing directory. This command gets the available fixtures for the specified testing directory:
$ slash list –only-fixtures path/to/tests
- temp_dir
Create a temporary directory
Source: path/to/tests/utilities.py:8