ut, stderr = self.ssh.exec_command("echo hello") stdin.close() self.assertEqual(stderr.read(), b"") self.assertEqual(stdout.read(), b"hello\n")
In the example above, we want to test various commands on a remote
VM. The fixture launches a new VM and configure an SSH
connection. However, if the SSH connection cannot be established, the
fixture will fail and the
tearDown() method won’t be invoked. The VM
will be left running.
Instead, with pytest, we could do this:
@pytest.yield_fixture def vm(): r = VM('Test-VM') r.start() yield r r.destroy() @pytest.yield_fixture def ssh(vm): ssh = SSHClient() ssh.connect(vm.public_ip) yield ssh ssh.close() def test_hello(ssh): stdin, stdout, stderr = ssh.exec_command("echo hello") stdin.close() stderr.read() == b"" stdout.read() == b"hello\n"
The first fixture will provide a freshly booted VM. The second one will setup an SSH connection to the VM provided as an argument. Fixtures are used through dependency injection: just give their names in the signature of the test functions and fixtures that need them. Each fixture only handle the lifetime of one entity. Whatever a dependent test function or fixture succeeds or fails, the VM will always be finally destroyed.
If you want to run the same test several times with a varying parameter, you can dynamically create test functions or use one test function with a loop. With pytest, you can parametrize test functions and fixtures:
@pytest.mark.parametrize("n1, n2, expected", [ (1, 3, 4), (8, 20, 28), (-4, 0, -4)]) def test_addition(n1, n2, expected): assert n1 + n2 == expected
The general plan for to test a feature in lldpd is the following:
- Setup two namespaces.
- Create a virtual link between them.
- Spawn a
lldpdprocess in each namespace.
- Test the feature in one namespace.
- Check with
lldpcliwe get the expected result in the other.
Here is a typical test using the most interesting features of pytest:
@pytest.mark.skipif('LLDP-MED' not in pytest.config.lldpd.features, reason="LLDP-MED not supported") @pytest.mark.parametrize("classe, expected", [ (1, "Generic Endpoint (Class I)"), (2, "Media Endpoint (Class II)"), (3, "Communication Device Endpoint (Class III)"), (4, "Network Connectivity Device")]) def test_med_devicetype(lldpd, lldpcli, namespaces, links, classe, expected): links(namespaces(1), namespaces(2)) with namespaces(1): lldpd("-r") with namespaces(2): lldpd("-M", str(classe)) with namespaces(1): out = lldpcli("-f", "keyvalue", "show", "neighbors", "details") assert out['lldp.eth0.lldp-med.device-type'] == expected
First, the test will be executed only if
lldpd was compiled with
LLDP-MED support. Second, the test is parametrized. We will execute
four distinct tests, one for each role that
lldpd should be able to
take as an LLDP-MED-enabled endpoint.
The signature of the test has four parameters that are not covered by
links. They are fixtures. A lot of magic happen in those to keep
the actual tests short:
lldpdis a factory to spawn an instance of
lldpd. When called, it will setup the current namespace (setting up the chroot, creating the user and group for privilege separation, replacing some files to be distribution-agnostic, …), then call
lldpdwith the additional parameters provided. The output is recorded and added to the test report in case of failure. The module also contains the creation of the
pytest.config.lldpdobject that is used to record the features supported by
lldpdand skip non-matching tests. You can read
fixtures/programs.pyfor more details.
lldpcliis also a factory, but it spawns instances of
lldpcli, the client to query
lldpd. Moreover, it will parse the output in a dictionary to reduce boilerplate.
namespacesis one of the most interesting pieces. It is a factory for Linux namespaces. It will spawn a new namespace or refer to an existing one. It is possible to switch from one namespace to another (with
with) as they are contexts. Behind the scene, the factory maintains the appropriate file descriptors for each namespace and switch to them with
setns(). Once the test is done, everything is wipped out as the file