Writing new test

The tests are written using the pytest framework, pytest-mh plugin and SSSD specific extensions that implements related hosts and roles.

This article covers the basic knowledge required to write a new test. After you finish it, make sure to go through the Crash Course and How to guides. You will also benefit from reading the API Reference.

See also

It is highly recommended to read pytest and pytest-mh documentation so you can write your tests with all features and tools that are available.

Using the topology marker

Each test that requires access to hosts defined in multihost configuration must be marked with a topology marker. This marker provides information about the topology that is required to run the test and defines fixture mapping between a short fixture name and a host from the multihost configuration (this is explained later in Deep dive into multihost fixtures).

The marker is used as:

import pytest


@pytest.mark.topology(name, topology, fixtures ...)
def test_example():
    assert True

Where name is the human-readable topology name that is visible in pytest verbose output, you can also use this name to filter tests that you want to run (with the -k or --mh-topology parameter). The next argument, topology, is instance of pytest_mh.Topology and then follows keyword arguments as a fixture mapping - this part is covered later.

See also

You can read more about the topology marker at pytest_mh, specifically at pytest_mh.TopologyMark. It is also worth to read the complete documentation of pytest_mh module.

There is a number of predefined topologies in sssd_test_framework.topology.KnownTopology that can be used directly as the topology marker argument. It is recommended to use this instead of providing your own topology unless it is really necessary.

import pytest

from sssd_test_framework.topology import KnownTopology
from sssd_test_framework.roles.client import Client
from sssd_test_framework.roles.ldap import LDAP


@pytest.mark.topology(KnownTopology.LDAP)
def test_example(client: Client, ldap: LDAP):
    assert True

The example above already uses the fixture mapping mentioned earlier. It uses the fixture client that points to the client host and ldap that can be used to manipulate with the host that provides the ldap role. This is thoroughly covered in the next section.

Deep dive into multihost fixtures

The previous example showed how to use sssd_test_framework.topology.KnownTopology.LDAP to define the required topology and provide client and ldap fixtures. This section described the mechanics underneath so you can correctly write your own tests.

Defining a topology

Simply put, topology defines the requirements that must be matched by multihost configuration in order to run the selected test. If the requirements are not fulfilled, the test is omitted.

The requirements are:

  • How many domains are needed

  • What domain ids are needed

  • How many hosts of specific role are needed inside a domain

For example the following topology (written in yaml) requires one domain of id sssd and the domain must contain one host that has the client role and one host that has the ldap role.

- id: sssd
  hosts:
    client: 1
    ldap: 1

There are pytest_mh.Topology and pytest_mh.TopologyDomain that you can use to put it in the code:

Topology(
    TopologyDomain('sssd', client=1, ldap=1)
)

Using the mh fixture

Warning

Using the mh fixture directly is not recommended. Please see Using dynamic fixtures to learn how to avoid using this fixture by creating a fixture mapping.

The pytest_mh.mh() is a fixture that is always available to a test that is marked with the topology marker. It provides access to domains by id and to hosts by role. Each host object is created as an instance of specific sssd_test_framework.roles.

We can use this fixture to access either group of hosts with mh.$domain-id.$role or individual host with mh.$domain-id.$role[$index]. The following snippet shows how to access the hosts from our example topology.

import pytest

from pytest_mh import Multihost, Topology, TopologyDomain


@pytest.mark.topology('ldap', Topology(TopologyDomain('sssd', client=1, ldap=1)))
def test_example(mh: Multihost):
    assert mh.sssd.client[0].role == 'client'
    assert mh.sssd.ldap[0].role == 'ldap'

We can also take advantage of Python type hints to let our editor provide us code suggestions.

import pytest

from pytest_mh import Multihost, Topology, TopologyDomain

from sssd_test_framework.roles.client import Client
from sssd_test_framework.roles.ldap import LDAP


@pytest.mark.topology('ldap', Topology(TopologyDomain('sssd', client=1, ldap=1)))
def test_example(mh: Multihost):
    client: Client = mh.sssd.client[0]
    ldap: LDAP = mh.sssd.ldap[0]

    assert client.role == 'client'
    assert ldap.role == 'ldap'

Once the test run is finished, this fixture automatically initiates a teardown process that rollbacks any change done on the remote host.

Using dynamic fixtures

Warning

Creating custom topologies and fixture mapping is not recommended and should be used only when it is really needed. See the following section Using known topologies to learn how to use predefined topologies in order to shorten the code and provide naming consistency across all tests.

The topology marker allows us to create a mapping between our own fixture name and specific path inside the mh fixture by providing additional keyword-only arguments to the marker.

The example above can be rewritten as:

import pytest

from pytest_mh import Topology, TopologyDomain

from sssd_test_framework.roles.client import Client
from sssd_test_framework.roles.ldap import LDAP


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

By adding the fixture mapping, we tell pytest_mh to dynamically create client and ldap fixtures for the test run and set it to the value of individual hosts inside the mh fixture which is still used under the hood.

We can also make a fixture for a group of hosts if our test would benefit from it.

import pytest

from pytest_mh import Topology, TopologyDomain

from sssd_test_framework.roles.client import Client


@pytest.mark.topology(
    'ldap', Topology(TopologyDomain('sssd', client=1, ldap=1)),
    clients='sssd.client'
)
def test_example(clients: list[Client]):
    for client in clients:
        assert client.role == 'client'

Note

We don’t have to provide mapping for every single host, it is up to us which hosts will be used. It is even possible to combine fixture mapping and at the same time use mh fixture as well:

def test_example(mh: Multihost, clients: list[Client])

It is also possible to request multiple fixtures for a single host. This can be used in test parametrization as we will see later.

@pytest.mark.topology(
    'ldap', Topology(TopologyDomain('sssd', client=1, ldap=1)),
    ldap='sssd.ldap[0]', provider='sssd.ldap[0]'
)

Using known topologies

This article already covered lots of ways of achieving the same thing to show how the plugin works. This section now describes the recommended usage by introducing sssd_test_framework.topology.KnownTopology class.

This class provides predefined pytest_mh.TopologyMark that can be used directly as parameter to the topology marker. Under the hood, it is the very same thing that was already explained.

The topology from previous examples is simply sssd_test_framework.topology.KnownTopology.LDAP. And we can use it like:

import pytest

from sssd_test_framework.topology import KnownTopology
from sssd_test_framework.roles.client import Client
from sssd_test_framework.roles.ldap import LDAP


@pytest.mark.topology(KnownTopology.LDAP)
def test_example(client: Client, ldap: LDAP):
    assert client.role == 'client'
    assert ldap.role == 'ldap'

Note

If you get to a point when existing topologies are not enough, feel free to define a new one inside sssd_test_framework.topology.KnownTopology and use the new entry so it can be reused later by other test when needed.

Topology parametrization

We can run single test against multiple SSSD providers by topology parametrization. This is achieved by assigning multiple topology markers to a single test.

import pytest

from sssd_test_framework.topology import KnownTopology
from sssd_test_framework.roles.client import Client
from sssd_test_framework.roles.generic import GenericProvider

@pytest.mark.topology(KnownTopology.LDAP)
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.topology(KnownTopology.AD)
@pytest.mark.topology(KnownTopology.Samba)
def test_example(client: Client, provider: GenericProvider):
    assert True

Now, if we run the test, we can see that it was executed multiple times and each time with a different topology. Therefore the provider points to the expected host (sssd.ldap[0] for ldap, sssd.ipa[0] for ipa etc.).

Note

It is best practice to mark as many topologies as possible, triggering multiple providers, when the test case allows it.

$ pytest --mh-config=mhc.yaml -k test_example -v
...
tests/test_basic.py::test_example (samba) PASSED                                                                                                                                                                                [ 12%]
tests/test_basic.py::test_example (ad) PASSED                                                                                                                                                                                   [ 25%]
tests/test_basic.py::test_example (ipa) PASSED                                                                                                                                                                                  [ 37%]
tests/test_basic.py::test_example (ldap) PASSED
...

This is internally achieved by providing two fixtures for the server host. We can look at how sssd_test_framework.topology.KnownTopology.LDAP is defined to see an example:

LDAP = TopologyMark(
    name='ldap',
    topology=Topology(TopologyDomain('sssd', client=1, ldap=1)),
    fixtures=dict(client='sssd.client[0]', ldap='sssd.ldap[0]', provider='sssd.ldap[0]')
)

We can go even further and use @pytest.mark.parametrize to test against multiple values.

import pytest

from sssd_test_framework.topology import KnownTopology
from sssd_test_framework.roles import Client, GenericProvider

@pytest.mark.parametrize('mockvalue', [1, 2])
@pytest.mark.topology(KnownTopology.LDAP)
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.topology(KnownTopology.AD)
@pytest.mark.topology(KnownTopology.Samba)
def test_example(client: Client, provider: GenericProvider, mockvalue: int):
    assert True

Now the test is run for each topology twice, once with mockvalue=1 and the second time with mockvalue=2.

$ pytest --mh-config=mhc.yaml -k test_example -v
...
tests/test_basic.py::test_example[1] (samba) PASSED                                                                                                                                                                                [ 12%]
tests/test_basic.py::test_example[1] (ad) PASSED                                                                                                                                                                                   [ 25%]
tests/test_basic.py::test_example[1] (ipa) PASSED                                                                                                                                                                                  [ 37%]
tests/test_basic.py::test_example[1] (ldap) PASSED                                                                                                                                                                                 [ 50%]
tests/test_basic.py::test_example[2] (samba) PASSED                                                                                                                                                                                [ 62%]
tests/test_basic.py::test_example[2] (ad) PASSED                                                                                                                                                                                   [ 75%]
tests/test_basic.py::test_example[2] (ipa) PASSED                                                                                                                                                                                  [ 87%]
tests/test_basic.py::test_example[2] (ldap) PASSED
...

Note

The previous examples can be made shorter by using sssd_test_framework.topology.KnownTopologyGroup, which groups multiple topologies together so they can be used in parametrization. For example:

import pytest

from sssd_test_framework.topology import KnownTopologyGroup
from sssd_test_framework.roles import Client, GenericProvider

@pytest.mark.parametrize('mockvalue', [1, 2])
@pytest.mark.topology(KnownTopologyGroup.AnyProvider)
def test_example(client: Client, provider: GenericProvider, mockvalue: int):
    assert True

See also

This article explained how to define a new test case and integrate it with the multihost plugin in order to run tests that require access to multiple machines, however it did not provide any information on how to actually run commands on remote hosts. This is explained in articles in How to guides, especially in Using multihost roles.