Extending pytest-mh
There are five main classes that are used by the pytest-mh
plugin that give
you access to remote hosts and provide you tools to build your own API that
fulfills specific requirements.
By extending these classes, you can provide your own functionality and configuration options.
MultihostConfig
: top level class that reads configuration and creates domain objectsMultihostDomain
: creates host objectsMultihostHost
: lives through the whole pytest session, gives low-level access to the hostMultihostRole
: lives only for a single test case, provides high-level APIMultihostUtility
: provides high-level API that can be shared between multiple rolesTopologyController
: control topology behavior such as per-topology setup and teardown
In order to start using pytest-mh
, you must provide at least your
own:MultihostConfig
to define what domain objects will be
created and MultihostDomain
to associate hosts and roles
with specific classes. It is recommended that you also extend the other classes
as well to provide high-level API for your tests.
Note
MultihostHost
, MultihostRole
and
MultihostUtility
have setup and teardown methods
that you can use to properly initialize the host and also to clean up
after the test is finished.
By extending these classes, you can give test writers a well-defined, unified API that can automate several tasks and make sure the hosts are properly setup before the test starts and all changes are correctly reverted once the test is finished.
This makes it easier to write new tests and ensure that the tests start with a fresh setup every time.
MultihostConfig
MultihostConfig
is created by pytest-mh
pytest plugin
during pytest session initialization. It reads the given multihost configuration
and creates the domain objects.
You must provide your own class that extends MultihostConfig
in order to use the plugin. Your class must override
id_to_domain_class
which creates your own
MultihostDomain
object.
Optionally, you can override
TopologyMarkClass
and provide your own
TopologyMark
class. With this, you can provide additional
information to the topology marker as needed by your project.
class ExampleMultihostConfig(MultihostConfig):
@property
def TopologyMarkClass(self) -> Type[TopologyMark]:
return ExampleTopologyMark
@property
def id_to_domain_class(self) -> dict[str, Type[MultihostDomain]]:
"""
Map domain id to domain class. Asterisk ``*`` can be used as fallback
value.
:rtype: Class name.
"""
return {"*": ExampleMultihostDomain}
MultihostDomain
MultihostDomain
is created by
MultihostConfig
and it allows you to associate roles from
your multihost configuration to your own hosts, roles, and Python classes to give
them meaning.
class ExampleMultihostDomain(MultihostDomain[ExampleMultihostConfig]):
def __init__(self, config: ExampleMultihostConfig, confdict: dict[str, Any]) -> None:
super().__init__(config, confdict)
@property
def role_to_host_class(self) -> dict[str, Type[MultihostHost]]:
"""
Map role to host class. Asterisk ``*`` can be used as fallback value.
:rtype: Class name.
"""
return {
"client": ClientHost,
"ldap": LDAPHost,
}
@property
def role_to_role_class(self) -> dict[str, Type[MultihostRole]]:
"""
Map role to role class. Asterisk ``*`` can be used as fallback value.
:rtype: Class name.
"""
return {
"client": Client,
"ldap": LDAP,
}
MultihostHost
One MultihostHost
object is created per each host defined in
your multihost configuration. Each host is created as an instance of a class
that is determined by the role to host mapping in
role_to_host_class()
.
This object gives you access to a SSH connection to the remote host. The object lives for the whole pytest session which makes it a good place to put functionality and data that must be available across all tests. For example, it can perform an initial backup of the host.
It provides two setup and teardown methods:
pytest_setup()
- called when pytest starts before execution of any testpytest_teardown()
- called when pytest terminated after all tests are donesetup()
- called before execution of each testteardown()
- called after a test is done
See also
See /example/lib/hosts/kdc.py to see an example implementation of custom host.
MultihostRole
Similar to MultihostHost
, one
MultihostRole
object is created per each host defined in
your multihost configuration. The difference between these two is that while
MultihostHost
lives for the whole pytest session,
MultihostRole
lives only for a single test run therefore the
role objects are not shared between tests. Role objects are also available to
you in your tests through pytest dynamic fixtures.
The purpose of the MultihostRole
object is to provide high
level API for your project that you can use in your tests and to perform
per-test setup and clean up. For this purpose, it provides setup and teardown
methods that you can overwrite:
setup()
- called before execution of each testteardown()
- called after a test is done
See also
See /example/lib/roles/kdc.py to see an example implementation of custom role.
MultihostUtility
Role object can also contain instances of MultihostUtility
that can be used to share functionality between individual roles. A
setup()
and
teardown()
methods are automatically called
after the role is setup and before the role teardown is executed.
Note
MultihostUtility
also contains
setup_when_used()
which is called only
after the class is first used inside the test (after
setup()
) and
teardown_when_used()
which is called only
if the class was used (before teardown()
).
This can be especially useful if the utility class is used only sporadically but the setup and teardown are quite expensive. In such case, you probably want to perform the setup and teardown only if the class was actually used in the test.
There are already some utility classes implemented in pytest-mh
. See
pytest_mh.utils
for more information on them.
See also
See /pytest_mh/utils/fs.py to see an implementation of a utility class that gives you access to files and directories on the remote host.
Each change that is made through the utility object (such as writing to a file) is automatically reverted (the original file is restored).
TopologyController
Topology controller can be assigned to a topology via @pytest.mark.topology or through known topology class. This controller provides various methods to control the topology behavior:
per-topology setup and teardown, called once before the first test/after the last test for given topology is executed
per-test topology setup and teardown, called before and after every test case for given topology
check topology requirements and skip the test if these are not satisfied
In order to use the controller, you need to inherit from
TopologyController
and override desired methods. Each method
can take any parameter as defined by the topology fixtures. The parameter value
is an instance of a MultihostHost
object.
See TopologyController
for API documentation
class ExampleController(TopologyController):
def skip(self, client: ClientHost) -> str | None:
result = client.ssh.run(
'''
# Implement your requirement check here
exit 1
''', raise_on_error=False)
if result.rc != 0:
return "Topology requirements were not met"
return None
def topology_setup(self, client: ClientHost):
# One-time setup, prepare the host for this topology
# Changes done here are shared for all tests
pass
def topology_teardown(self, client: ClientHost):
# One-time teardown, this should undo changes from
# topology_setup
pass
def setup(self, client: ClientHost):
# Perform per-topology test setup
# This is called before execution of every test
pass
def teardown(self, client: ClientHost):
# Perform per-topology test teardown, this should undo changes
# from setup
pass
class ExampleController(TopologyController):
# Implement methods you are interested in here
pass
@pytest.mark.topology(
"example", Topology(TopologyDomain("example", client=1)),
controller=ExampleController(),
fixtures=dict(client="example.client[0]")
)
def test_example(client: Client):
pass
class ExampleController(TopologyController):
# Implement methods you are interested in here
pass
@final
@unique
class KnownTopology(KnownTopologyBase):
EXAMPLE = TopologyMark(
name='example',
topology=Topology(TopologyDomain("example", client=1)),
controller=ExampleController(),
fixtures=dict(client='example.client[0]'),
)
@pytest.mark.topology(KnownTopology.EXAMPLE)
def test_example(client: Client):
pass
Setup and teardown
The following schema shows how individual setup and teardown methods of host, role, and utility objects are executed.