Writing Tests

Each test that should have access to the remote hosts must be marked with one or more topology markers. This tells pytest-mh what domains, hosts and roles are required to run the test. The marker also defines how the MultihostRole objects should be accessible from within the test.

The recommended way is to use “dynamic” fixtures, which are fixtures that do not exist anywhere in the code but are injected into the test parameters by pytest-mh. It is also possible to get the access through the mh() fixture, but this is quite low level and should be avoided, unless you have a valid use case for it.

See also

The topology, topology marker and related information is deeply covered in Multihost Topologies.

Using the mh fixture - low-level API

Warning

Using the mh() fixture directly is supported, but not recommended. You should avoid it unless you have a valid use case for it. However, it is recommended to read this section anyway in order to better understand how things work.

The mh() fixture is automatically available to every test and it returns an instance of MultihostFixture. This fixture internally takes care of calling test setup and teardown as well as collecting test artifacts. It does provide access to all the roles ands hosts, topology and the topology marker as well as other stuff that are needed for this fixture to do its job.

There are several attributes that you may find helpful if you need access to this object.

mh fixture attributes

Attribute name

Description

ns

Role objects accessible through namespace mh.ns.domain_id.role_name

logger

Multihost logger – log messages to test.log

roles

List of all role objects

hosts

List of all hosts objects

topology

Current topology assigned to the test

topology_mark

Current topology marker assigned to the test

multihost

Multihost configuration (instance of MultihostConfig)

Example usage of mh fixture
@pytest.mark.topology('ldap', Topology(TopologyDomain('test', client=1, ldap=1)))
def test_example(mh: MultihostFixture):
    assert mh.ns.test.client[0].role == 'client'
    assert mh.ns.test.ldap[0].role == 'ldap'

This fixture can be used also in all function-scoped pytest fixtures. The following example shows how to get direct access to the roles in the test. This, however, can be achieved by using pytest-mh’s dynamic fixtures and their mapping.

Example usage of mh fixture inside pytest fixture
@pytest.fixture
def client(mh: MultihostFixture) -> Client:
    return mh.ns.test.client[0]

@pytest.fixture
def ldap(mh: MultihostFixture) -> LDAP:
    return mh.ns.test.ldap[0]

@pytest.mark.topology('ldap', Topology(TopologyDomain('test', client=1, ldap=1)))
def test_example(client: Client, ldap: LDAP):
    assert client.role == 'client'
    assert ldap.role == 'ldap'

Note

Usually, there should not be any reason for you to access the mh() fixture directly. The roles are available to the tests if a fixture mapping is defined. They are also available in the function-scoped fixtures if the fixture is defined with mh_fixture() decorator instead of @pytest.fixture (see: Using Pytest Fixtures).

Most of the other properties are available as standalone fixtures. Go to Built-in fixtures to see the list of available fixtures.

Using dynamic fixtures - high-level API

The topology marker has a fixtures parameter that defines a mapping between custom fixture names and specific multihost roles that are required by the topology. Therefore, instead of accessing the mh() fixture and defining custom fixtures as a shortcut to the role objects, we can define the mapping directly in the topology marker:

@pytest.mark.topology(
    'ldap', Topology(TopologyDomain('test', client=1, ldap=1)),
    fixtures=dict(client='test.client[0]', ldap='test.ldap[0]')
)
def test_example(client: Client, ldap: LDAP):
    assert client.role == 'client'
    assert ldap.role == 'ldap'
@pytest.fixture
def client(mh: MultihostFixture) -> Client:
    return mh.ns.test.client[0]

@pytest.fixture
def ldap(mh: MultihostFixture) -> LDAP:
    return mh.ns.test.ldap[0]

@pytest.mark.topology('ldap', Topology(TopologyDomain('test', client=1, ldap=1)))
def test_example(client: Client, ldap: LDAP):
    assert client.role == 'client'
    assert ldap.role == 'ldap'

The fixtures are referred to as “dynamic” because they do not exist anywhere as a standalone pytest fixture function. They are dynamically created by pytest-mh for each test and the same name refers to a different object in each test. They can even point to a different host.

@pytest.mark.topology(
    'ldap-a', Topology(TopologyDomain('test', client=1, ldap=1)),
    fixtures=dict(
        client='test.client[0]',
        ldap='test.ldap[0]'
    )
)
def test_example_a(client: Client, ldap: LDAP):
    assert client.role == 'client'

    # ldap points to the first host with role ldap found in the test domain
    assert ldap.role == 'ldap'

@pytest.mark.topology(
    'ldap-b', Topology(TopologyDomain('test', client=1, ldap=1)),
    fixtures=dict(
        client='test.client[0]',
        ldap='test.ldap[1]'
    )
)
def test_example_b(client: Client, ldap: LDAP):
    assert client.role == 'client'

    # ldap points to the second host with role ldap found in the test domain
    assert ldap.role == 'ldap'

Fixture path

The fixture path is in the form of domain-id.role-name[index]. The index refers to a specific host in the order defined by current mhc.yaml and it starts from zero. The index path can be omitted, in this case it gives you access to the list of all hosts that implements this role.

@pytest.mark.topology(
    'ldap-a', Topology(TopologyDomain('test', client=1, ldap=4)),
    fixtures=dict(
        client='test.client[0]',
        ldap='test.ldap[0]',
        all_ldaps='test.ldap'
    )
)
def test_example_a(client: Client, ldap: LDAP, all_ldaps: list[LDAP]):
    assert client.role == 'client'

    assert ldap.role == 'ldap'
    assert ldap in all_ldaps

How to write a test

Previous sections showed how the things around multihost topologies works, so how should you write a new test? Just follow these steps:

  1. Choose the topology or list of topologies that the test will use

  2. Define the topology outside the test so it can be reused (the topology is most likely already defined in the project)

  3. Write a skeleton using the topology

  4. Write the test body

Note

It is recommended to use a predefined topology marker so the topology can be easily shared between tests. See Multihost Topologies for more information.

Test skeleton
from framework.topology import KnownTopology

@pytest.mark.topology(KnownTopology.LDAP)
def test_skeleton(client: Client, ldap: LDAP):
    pass

The test can also use a topology parametrization, which can run the test once per each topology. This is achieved by using a topology group or assigning more then one topology to the test.

Test skeleton
from framework.topology import KnownTopologyGroup

@pytest.mark.topology(KnownTopology.AnyProvider)
def test_skeleton(client: Client, provider: GenericProvider):
    pass
Test skeleton
from framework.topology import KnownTopology

@pytest.mark.topology(KnownTopology.LDAP)
@pytest.mark.topology(KnownTopology.SSSD)
@pytest.mark.topology(KnownTopology.Sudoers)
def test_skeleton(client: Client, provider: GenericProvider):
    pass

Built-in fixtures

Built-in fixtures

Fixture name

Return Type

Description

mh

MultihostFixture

Low level pytest-mh object.

mh_config

MultihostConfig

Main multihost configuration object.

mh_logger

MultihostLogger

Multihost logger, can be used to write messages into the test log.

mh_topology

Topology

Current test’s topology object.

mh_topology_name

str

Current test’s topology name.

mh_topology_mark

TopologyMark

Current test’s topology marker object.