Fixtures
Fixtures provide data, objects or services for tests. They are automatically initialized and uninitialized by the test runner.
Fixtures have a scope:
'test'
: Fixture is initialized and uninitialized for every single test. Smallest scope.'class'
: Fixture is initialized when a class holding tests is run for the first time, and uninitialized after all tests of the are run.'module'
: Fixture is initialized once for all tests in a module.'session'
: Fixture is initialized for the whole session. Greatest scope.
Fixtures are implemented as python functions returning an object or python generators yielding a single object. Python generators offer set up and tear down code while python function fixture only offer set up code.
To use a fixture in a test, simply add a parameter with the fixture’s name to the test. The yielded object is passed to the test when it is executed by the test runner.
Fixtures are collected automatically by the test runner by searching the test specifiers.
To create a fixture decorate it with htf.fixture
.
@htf.fixture('test')
def server() -> Generator["Server", None, None]:
server = Server()
yield server
server.stop()
# pass a parameter with the fixture's name to use the fixture
def test_server(server):
server.handle_request()
Fixtures can also depend on other fixtures. To create a dependency simply add a parameter to the fixture like you would for a test.
@htf.fixture('test')
def client(server):
return Client()
def test_client(client, server):
server.run()
client.connect()
Circular dependencies are detected and raise an exception.
Requested fixtures must have the same or a greater scope than the requesting fixture. I.e. a fixture with scope ‘test’ can request fixtures with all other scopes, but a fixture with scope ‘session’ can only request other fixtures with ‘session’ scope.
You can change the fixture name by setting the name
parameter.
Generally fixtures are only called when specifically requested by tests or other fixtures. For auxillary fixtures
which require no direct interaction, you can set the auto
parameter to True
. In this case they are set up
and torn down when their scope is entered or left.
Fixtures can be async, too.
@htf.fixture('test')
async def async_fixture() -> AsyncGenerator["Server", None]:
server = await Server.create()
yield server
await server.stop()
Creating Fixtures
To create fixtures develop them using the htf.fixture
decorator.
Using Fixtures
To use fixtures, add the sources containing the fixtures as test specifier
to the htf
command line utility or htf.main
.
The fixtures are collected automatically.
Fixture Decorator
The htf.fixture
decorator is used to create fixtures.
- htf.fixture(scope: str, name: str | None = None, auto: bool = False) Callable[[...], Any]
Decorator to mark a function as a fixture.
- Parameters:
scope – the scope determining the life-time of the fixture. Choose out of ‘session’, ‘module’, ‘class’ and ‘test’.
name=None – the name to be used for the fixture
auto=False – automatically call the fixture when its scope is entered, instead of when requested
Built-in Fixtures
htf is shipped with many built-in fixtures for different use cases.
Assertions (assertions)
assertions
lets you easily do assertions. The fixture is htf.assertions
.
The scope is 'session'
.
def test_with_assertions(assertions):
"""
Args:
assertions (htf.assertions): the assertions fixture
"""
assertions.assert_true(True)
File and URL Attachments (attachments)
htf.fixtures.attachments
lets you attach files and URLs to a test report. The scope is 'test'
.
def test_attach_file(attachments):
"""
Args:
attachments (htf.fixtures.attachments): the attachments fixture
"""
attachments.attach_file("example.jpg", "an example image attached to a report")
Attaching files as bytes
def test_attach_file_as_bytes(attachments):
"""
Args:
attachments (htf.fixtures.attachments): the attachments fixture
"""
filename = "example.png"
mime_type = "image/png"
with open(filename, "rb") as f:
data_bytes = f.read()
attachments.attach_bytes("Example", data_bytes, filename, mime_type)
Attaching a URL
def test_attach_url(attachments):
"""
Args:
attachments (htf.fixtures.attachments): the attachments fixture
"""
attachments.attach_url("https://hilster.io", "an example URL attached to a report")
- class htf.fixtures.attachments(result: TestResult)
Attachments to the test report. Scope is
'test'
.- attach_bytes(title: str, data_bytes: bytes, filename: str, mime_type: str) None
Accepts bytes and mime type and converts them to a data url and attaches it to the result.
- Parameters:
title – the title
data_bytes – the bytes to attach
filename – the file name
mime_type – MIME type of the data bytes
- attach_file(filename: str, title: str) None
Attach a file to the current test that is put into the test report.
- Parameters:
filename – the filename to be attached
title – the title
To increase the maximum attachment size simply create a new fixture with the maximum size set to the desired value:
@htf.fixture('test')
def big_attachments(attachments):
attachments.set_maximum_attachment_size(100*1024*1024) # 100 MB
yield attachments
Context (context)
The htf.fixtures.context
is used to share data between steps within a behavior driven test.
The scope is 'test'
.
- class htf.fixtures.context
A context fixture used to share context data in behavior driven steps.
@htf.given("the DUT is up and running")
def up_and_running(context):
context.dut = DUT()
@htf.when("button is pressed")
def button_is_pressed(context):
context.dut.press_button()
@htf.then(r"the button state shall be '([a-z]+)'")
def button_state(pressed, context):
htf.assert_equal(context.dut.get_button_state(), pressed)
Delay Functions (delay)
htf.fixtures.delay
lets you delay tests. The remaining delay is printed to the terminal.
htf.fixtures.delay
is callable. The scope is 'session'
.
def test_with_delay(delay):
"""
Args:
delay (htf.fixtures.delay): the delay fixture
"""
delay(5)
- class htf.fixtures.delay
Delay execution for a given duration and print progress. Scope is
'session'
.- Parameters:
duration – delay duration in seconds
Sleep Functions (async) (sleep)
htf.fixtures.sleep
lets you delay tests. The remaining delay is printed to the terminal.
htf.fixtures.sleep
is callable. The scope is 'session'
.
async def test_with_sleep(desleeplay):
"""
Args:
sleep (htf.fixtures.sleep): the sleep fixture
"""
await sleep(5)
- class htf.fixtures.sleep
Delay execution for a given duration and print progress. Used in async context only. Scope is
'session'
.- Parameters:
duration – delay duration in seconds
Interactive Testing (interaction)
The htf.fixtures.interaction
fixture is documented in the Interactive Testing section.
Issue Tracking (jira)
The usage of the htf.fixtures.jira
fixture is documented in the JIRA (Atlassian) section.
Append Metadata to Test Reports (metadata)
htf.fixtures.metadata
lets you add metadata to tests.
htf.fixtures.metadata
works like a dict
but also support setters and getters.
The scope is 'test'
.
def test_with_metadata(metadata):
"""
Args:
metadata (htf.fixtures.metadata): the metadata fixture
"""
metadata.set("key", "value")
metadata["Date"] = datetime.now()
Access Settings (settings)
Settings
lets you easily access settings in a test.
The returned object is an instance of htf.Settings
.
The scope is 'session'
.
def test_with_settings(settings):
"""
Args:
settings (htf.fixtures.settings): the settings fixture
"""
print(settings.example_setting)
Access Testrun Parameters (parameters)
Parameters
lets you easily access test run parameters in a test.
The returned object is an instance of a read-only dict like object.
The scope is 'session'
.
def test_parameters(parameters):
"""
Args:
settings (htf.fixtures.parameters): the parameters fixture
"""
print(parameters['name'])
# htf --parameter name=foobar
# htf.main(parameters=dict(name="foobar"))
Parameters can also be used to control the behavior of other fixtures.
@htf.fixture(scope="test")
def io(parameters):
if parameters.get('io_enable', default=False):
yield IO(parameters['io_settings'])
else:
yield SimulatedIO(parameters['io_settings'])
Structure Tests (step)
htf.fixtures.step
lets you separate your test into steps.
The scope is 'test'
.
def test_with_step(step):
"""
Args:
step (htf.fixtures.step): the step fixture
"""
with step("First step"):
htf.assert_true(True)
# steps can be nested, too
with step("Outer step"):
with step("Inner step"):
htf.assert_true(True)
# steps can have an expected result
with step("Succeeding step", expected_result="this step succeeds"):
htf.assert_true(True)
You can also add additional data to steps that will be put into the test report.
def test_with_additional_step_data(step):
"""
Args:
step (htf.fixtures.step): the step fixture
"""
with step("First step", 1, 2, 3, step_id=100):
htf.assert_true(True)
To skip a step run
def test_with_skipped_step(step):
"""
Args:
step (htf.fixtures.step): the step fixture
"""
with step("Skipped step"):
step.skip("This step is skipped.")
htf.fixtures.step
supports expected and actual results, too.
def test_with_expected_and_actual_result(step):
"""
Args:
step (htf.fixtures.step): the step fixture
"""
with step("Step with expected and actual result", expected_result="The expected result") as current_step:
current_step.set_actual_result("the actual result")
htf.fixtures.step
can be used as a dict
to add data to the test result at runtime.
The default contents are read-only.
def test_with_expected_and_actual_result(step):
"""
Args:
step (htf.fixtures.step): the step fixture
"""
with step("Step with expected and actual result", expected_result="The expected result") as current_step:
current_step["actual_result"] = "the actual result"
- class htf.fixtures.step(result: TestResult)
Run a test step that is documented in the test report. Scope is
'test'
.- Parameters:
title – the step’s title.
callable_obj=None – a callable object that is called within the step or
None
.*args – a tuple of positional arguments for the step’s context.
**kwargs – a dictionary of keyword arguments for the step’s context.
Test Information (test)
htf.fixtures.test
gives you access to information about the current test.
The scope is 'test'
.
def test_info(test):
"""
Args:
test (htf.fixtures.test): the test fixture
"""
print("Test function name:", test.name)
print("Test class name:", test.class_name)
print("Test module:", test.module)
print("Docstring:", test.doc)
print("Tags:", test.tags)
print("Current stdout:", test.stdout)
print("Current stderr:", test.stderr)
print("Current result:", test.result) # None
The properties
test.result
,
test.was_successful
,
test.was_not_successful
,
test.was_failure
,
test.was_error
,
test.exception_type
,
test.exception_value
and
test.exception_traceback
are only available after the test was run in the context of another fixture using
the test
fixture.
Thus, it is possible to add actions in fixtures depending on the test’s result.
@htf.fixture(scope="test", auto=True)
def after_failed_test(test):
yield
if test.was_not_successful:
print(f"The test {test.name} was not successful! It failed with {test.exception_type.__class__.__name__}: '{test.exception_value}'")
def test_failure(test):
assert False, "This test fails"
- class htf.fixtures.test(result: TestResult)
Information about the running test. Scope is
'test'
.
Feature Information (feature)
htf.fixtures.feature
gives you access to information about the current feature running in a
behavior driven test.
The scope is 'test'
.
@htf.given("when I would like to see feature information")
def given_feature(feature):
"""
Args:
test (htf.fixtures.feature): the feature fixture
"""
print("Feature name:", feature.name)
print("Feature doc:", feature.doc)
print("Feature filename:", feature.filename)
- class htf.fixtures.feature(result: TestResult)
Information about the running feature in behavior driven testing. Scope is
'test'
.
Scenario Information (scenario)
htf.fixtures.scenario
gives you access to information about the current scenario running in a
behavior driven test.
The scope is 'test'
.
@htf.given("when I would like to see scenario information")
def given_feature(scenario):
"""
Args:
test (htf.fixtures.scenario): the scenario fixture
"""
print("Scenario name:", scenario.name)
- class htf.fixtures.scenario(result: TestResult)
Information about the running scenario in behavior driven testing. Scope is
'test'
.
Statistics (statistics)
htf.fixtures.statistics
gives you statistics about the current test session.
The scope is 'session'
.
def test_statistics(statistics):
"""
Args:
statistics (htf.fixtures.statistics): the statistics fixture
"""
print("Total number of tests:", statistics.number_of_tests)
print("Tests run so far:", statistics.tests_run)
print("Percent of tests finished:", statistics.done_percent)
print("Successes:", statistics.successes)
print("Failures:", statistics.failures)
print("Errors:", statistics.errors)
print("Skipped:", statistics.skipped)
print("Percent of successes:", statistics.success_percent)
print("Percent of failures:", statistics.failure_percent)
print("Percent of errors:", statistics.error_percent)
print("Percent of skipped:", statistics.skipped_percent)
- class htf.fixtures.statistics(statistics_provider: statistics_provider)
Statistics about the test session. Scope is
'session'
.
Run Threads (threads)
htf.fixtures.threads
lets you start threads that are stopped automatically.
You can start continuously running threads and periodically executed threads.
The scope is 'test'
. All started threads are automatically stopped after the starting test ends.
You do not need to worry about stopping threads yourself.
def test_with_background_thread(threads):
"""
Args:
threads (htf.fixtures.threads): the threads fixture
"""
def background_thread():
while True:
print("running in background")
threads.run_background(background_thread)
def test_with_periodic_thread(threads):
"""
Args:
threads (htf.fixtures.threads): the threads fixture
"""
def periodic_thread():
print("running periodically in background")
threads.run_periodic(periodic_thread, period=1.0)
time.sleep(5)
Automatic stoppable threads are instrumented to be stoppable under all circumstances. Thus the performance may be impacted
but while True
loops are allowed in these threads, the development is very easy and you do not need any
overhead to stop the thread.
If you need more performance for background threads you have to make your threads stoppable from the outside and not
use while True
loops.
def test_stoppable_thread(threads):
# create an event to stop the thread
stop_condition = threading.Event()
def background_thread():
while not stop_condition.is_set():
print("running ..")
time.sleep(.5)
# normally started threads that is stopped automatically with reduced performance
threads.run_background(background_thread)
# high performance background thread that
stop = threads.run_background(background_thread, stop=stop_condition.set, force_stop=False)
# wait
time.sleep(2.0)
# stop the thread explicitly
stop()
- class htf.fixtures.threads
Fixture for threads with scope ‘test’ that are stopped automatically and can run in background and periodically.
- run_background(func: Callable[[...], Any], name: str | None = None, stop: Callable[[...], bool] | None = None, force_stop: bool = True, join_timeout: float = 1.0, args: Tuple[Any] | None = None, kwargs: Dict[str, Any] | None = None) Callable[[], None]
With this method you can run another method in background.
- Parameters:
func – the method to be run in background.
name=None – a name for the background task. Default:
None
. If name isNone
func.__name__ is used.stop=None – callable to stop the thread
force_stop=True – force the thread to stop when leaving its scope (can impact performance)
join_timeout=1 – the timeout to be used when calling
join
on the thread.args=None – a tuple of positional arguments that are passed to the background task when it is called.
kwargs=None – a dictionary of keyword arguments that are passed to the background task when it is called.
- Returns:
- a callable object is returned that stops
execution when called.
- Return type:
stop-trigger
- run_periodic(func: Callable[[...], Any], period: float, maximum_period: float | None = None, name: str | None = None, raise_exception: bool = True, args: Tuple[Any] | None = None, kwargs: Dict[str, Any] | None = None) Callable[[], None]
With this method you can run another method periodically in background with a given period.
- Parameters:
func – the method to be called periodically in background.
period – the period in second.
maximum_period=None – if set to a float >=
period
func
may take up tomaximum_period
time without raising an exception. A warning is printed out to stdout instead.name=None – a name for the background task.
raise_exceptions=True – if set to
True
exceptions are put into the exception queue to let a test fail if a background thread fails.args=None – a tuple of positional arguments that are passed to the background task when it is called.
kwargs=None – a dictionary of keyword arguments that are passed to the background task when it is called.
- Returns:
- a callable object is returned that stops
execution when called.
- Return type:
stop-trigger
- stop() None
This method stops all background threads by calling
thread.join()
until it is not alive anymore.
- stop_thread(name_or_function: str | Callable) int
This method stops a specific thread with the name or function described by
name_or_function
by callingjoin
with join timeout supplied byrun_periodic
orrun_background
or on it. It does not set a stop condition so the thread must end and must not include an endless loop. If the thread is still alive after the first join messages are printed to inform the user.
Run Session Threads (session_threads)
Like htf.fixtures.threads
but with scope 'session'
.
Run Processes (processes)
htf.fixtures.processes
lets you start processes that are stopped automatically.
You can start single background processes and process pools. It is based on multiprocessing
.
The scope is 'test'
. All started processes are automatically stopped after the starting test ends.
You do not need to worry about stopping processes yourself.
Create a single background process:
def test_process_stopping(processes):
"""
Test a stopping background process.
Args:
processes (htf.fixtures.processes): the processes fixture
"""
def process(*what):
print("process is running ..")
for i in range(2):
print(" ".join(what))
time.sleep(1.0)
processes.run(target=process, args=("a", "b"))
time.sleep(3.0)
Create a pool and run tasks in parallel:
def square(x):
return x**2
def test_pool(processes):
"""
Test processes fixture's pool.
Args:
processes (htf.fixtures.processes): the processes fixture
"""
pool = processes.pool(processes=2)
print(pool.map(square, range(10)))
time.sleep(1.0)
- class htf.fixtures.processes
Fixture for background processes with scope
'test'
that are stopped automatically. Usesmultiprocessing
.- pool(processes: int | None = None, initializer: Callable[[...], Any] | None = None, initargs: Iterable[Any] = (), maxtasksperchild: int | None = None, timeout: float = 1.0) multiprocessing.pool.Pool
Create a pool to run tasks in parallel.
- Parameters:
processes=None – the number of child processes to be started.
initializer=None – f initializer is not None then each worker process will call
initializer(*initargs)
when it starts.initargs=() – arguments for
initializer
.maxtasksperchild=None – the maximum number of tasks per child process.
timeout=1.0 – the timeout for joining the pool process.
- Returns:
the pool
- run(group: None = None, target: Callable[[...], Any] | None = None, name: str | None = None, args: Iterable[Any] = (), kwargs: Dict[str, Any] | None = None, daemon: bool | None = None, timeout: float = 1.0) Process
Run a background process.
- Parameters:
group=None – the group
target=None – A callable to be run in background. Must be pickleable.
name=None – the name of the background process.
args=() – the positional arguments passed to the background process callable.
kwargs=None – the keyword arguments passed to the background process callable.
daemon=None – if set to
True
the child process is terminated if the parent process ends.timeout=1.0 – the timeout for joining, terminating and killing the process.
- Returns:
the process
Run Session Processes (session_processes)
Like htf.fixtures.processes
but with scope 'session'
.
Type Hints for fixtures
To be able to use code completion in you IDE add type hints or docstrings to your tests so that the IDE is able to find the right fixture.
# type hint:
def test_step(step : htf.fixtures.step):
with step("A step"):
pass
# docstring:
def test_step(step):
"""
This test ...
Args:
step (htf.fixtures.step): the step fixture
"""
with step("A step"):
pass
Example: Cache Fixture
Assume you want to cache data over multiple test runs to reduce the runtime. You could create a cache fixture.
import json
# cache fixture
@htf.fixture('session')
def cache():
# set up
filename = "cache.json"
try:
with open(filename, "r") as fp:
data = json.load(fp)
except FileNotFoundError:
with open(filename, "w") as fp:
json.dump({}, fp)
print("Created cache file '{}'".format(filename))
data = {}
yield data
# tear down
with open(filename, "w") as fp:
json.dump(data, fp)
print("Wrote cache file '{}'".format(filename))
# test using cache fixture
def test_cache(cache):
try:
# count up test run number
cache["#"] += 1
except KeyError:
cache["#"] = 1
print("This is test run {}".format(cache["#"]))
Tagging Fixtures
Fixtures can also be tagged. Thus it is easy to select a set of fixture with a tag expression.
For example you can create fixtures for simulated and hardware in the loop devices with different tags and select a group to run the same tests with and without hardware.
htf
support -F <expression>
and --fixture-tags=<expression>
.
Unlike selecting tests by tags all untagged fixtures are included in the result.
So if you use fixture tags they will only be used to filter actually tagged
fixtures.
Example:
class Device:
pass
@htf.tags("simulation")
@htf.fixture('test', name="device")
def device_simulation():
print("simulated device")
yield Device()
@htf.tags("hardware")
@htf.fixture('test', name="device")
def device_hardware():
print("hardware device")
yield Device()
def test_device(device):
pass
if __name__ == "__main__":
htf.main(fixture_tags="hardware")
# $ htf --fixture-tags=hardware
Accessing Fixtures directly
You might need to access one fixture in a location where it is not already set up.
This is useful for fixtures that are not part of the argument list of the test function.
To do that, you can use htf.get_fixture
(async variant) or htf.get_fixture_sync
(sync variant).
If the fixture was not instantiated before, then it will be instantiated as soon as the upper functions are called.
For async (coroutine) fixtures, the following example shows the behavior.
@htf.fixture('test')
async def example_fixture():
yield "Example"
async def test_device():
fixture = await htf.get_fixture("example_fixture")
In case you don’t want to use coroutines, it is still possible to use the sync variant.
@htf.fixture('test')
def example_fixture():
yield "Example"
def test_device():
fixture = htf.get_fixture_sync("example_fixture")
Note
htf.get_fixture_sync
will raise an exception when trying to instantiate an async fixture.
It will however return already instantiated fixtures regardless of its type.
- async htf.get_fixture(name: str) Any
Returns a fixture object as identified by the name parameter. If a fixture requires other fixtures, they will be instantiated, if necessary. This is the async variant which returns a coroutine, so ideal for async fixtures.
- Parameters:
name – name of the requested fixture
- Returns:
fixture instance
- Raises:
Exception – if a circular dependency is detected during resolution