# Testinfra: Practical, Pytest-Friendly Infrastructure Testing

Modern infrastructure management requires consistency in the state of servers, and it's as crucial as the consistency of the code that runs on them.  This is where the principles of  **Test-Driven Infrastructure (TDI)** come into play, and **Testinfra** stands out as a powerful and elegant tool for implementing this practice. Testinfra is a Python framework that allows you to write unit tests for your infrastructure, verifying that your servers are configured exactly as you expect.

If you have used pytest to test your Python code, Testinfra will feel right at home.  Testinfra is a **pytest** plugin, which means it leverages the power and flexibility of this popular Python testing framework. It allows you to write clean, readable tests to verify the state of various server components, such as packages, services, files, network sockets, and more. Think of it as unit testing for your servers. Instead of testing a function or a class, you're testing the state of your infrastructure.

# Advantages of Testinfra

These are some of the benefits testinfra offers:

* **Idempotency and Consistency**: By writing tests for your infrastructure, you can ensure that your configuration management tools (like Ansible, Salt, Puppet, or Chef) are working correctly and that your servers are in a consistent and predictable state.
    
* **Early Detection of Errors**: Testinfra helps you catch configuration drifts and errors early in the development cycle, long before they can cause problems in production.
    
* **Improved Collaboration**: Tests serve as a form of documentation, clearly defining the expected state of your infrastructure and making it easier for teams to collaborate on managing and maintaining servers.
    
* **Auditability and compliance:** By creating reproducible tests that are created and maintained centrally, testinfra enables infrastructure and security engineers to ensure that the production infrastructure is in the expected states and is not prone to reliability and security incidents that happen due to manual changes.
    

# Modeling infrastructure tests

Fixtures provide a consistent context and reliable environment for tests.  In pytest, fixtures are part of the arrange phase. To know more about different phases of pytest, read the “[Anatomy of a test”](https://docs.pytest.org/en/stable/explanation/anatomy.html#test-anatomy).   The **host fixture** is the central element of testinfra. It represents the system under test and provides access to all the different modules that you can use to inspect the server's state.

Each capability is a **module** exposed as a method on the host.  Here are some of the most commonly used modules available through the host fixture:

* host.package(name): To check the status of a package.
    
* host.service(name): To check the status of a service.
    
* host.file(path): To inspect files and directories.
    
* host.socket(uri): To check for listening sockets.
    
* host.user(name): To get information about a user.
    
* host.group(name): To get information about a group.
    
* host.interface(name): To inspect network interfaces.
    
* host.process(name): To find and inspect processes.
    
* host.command(command): To run a command and inspect its output.
    

You declare the host as a test function argument and then call the module you need. For example, to check if the host is listening on port 22 ( for SSH ), write the test as follows:

```python
def test_ssh_port(host):
    assert host.socket("tcp://0.0.0.0:22").is_listening
```

# Getting started

Start by installing the pytest-testinfra package

```bash
pip install pytest-testinfra
```

Let us write a few tests to validate the localhost on which TestInfra is installed

Add the following tests to the file `local_infra.py`.  Description of each test is added as a comment

```python

# Test if the passwd file exists and  
# has the right user/group ownership and file permissions
def test_passwd_file(host):   
    f = host.file("/etc/passwd")   
    assert f.exists and f.user == "root" and f.group == "root" and f.mode == 0o644

# Check if the package openresty ( an nginx variant with lua ) is installed 
# and the package version is 1.2*
def test_openresty_installed(host):   
    pkg = host.package("openresty")   
    assert pkg.is_installed
    assert pkg.version.startswith("1.2")
 # Ensure openresty is running as a service and is enabled
def test_openresty_service(host):   
    s = host.service("openresty")
    assert s.is_running
    assert s.is_enabled
```

Now, let us test these

```bash

pytest -v local_infra.py  
===================================================== test session starts ========================================
<OUTPUT TRUNCATED>collected 3 items                                                                                                                                                                            
local_infra.py::test_passwd_file[local] PASSED                                                                                             [ 33%]
local_infra.py::test_openresty_installed[local] PASSED                                                                                     [ 66%]
local_infra.py::test_openresty_service[local] PASSED                                                                                       [100%]
===================================================== 3 passed in 0.13s =========================================
```

# Concept of backends

Testinfra supports a rich set of **connection backends**. By default, all tests are run **locally**, but you can target remote hosts or containers with --hosts=&lt;backend specification&gt;.

Supported backends are:

* SSH
    
* Paramiko - Python implementation of the SSHv2 protocol
    
* Ansible
    
* Docker
    
* Podman
    
* Kubernetes
    
* Openshift
    
* Salt
    
* WinRM
    
* LXC/LXD
    

Let's use SSH to connect to a remote host and do some validation

We will log in to a remote host 206.189.137.55 and check if the root login for SSH is disabled or not.  Passwordless SSH is already set up for the host. Create a test file `test_backend.py` and add the following:

```python
def test_ssh_no_root_login(host):
   sshd_config = host.file("/etc/ssh/sshd_config")
   assert sshd_config.contains("^PermitRootLogin no")
```

Run the test

```bash
pytest -vv --hosts="ssh://206.189.137.55"  test_backend.py  

========================================== test session starts ===========================
<OUTPUT TRUNCATED>
collected 1 item

test_backend.py::test_ssh_no_root_login[ssh://206.189.137.55] PASSED  

============================================ 1 passed in 1.15s ===========================
```

For most backends except SSH, you will have to install the corresponding testinfra module as follows:

```bash
pip install 'pytest-testinfra[ansible,salt]'
```

You can use pytest parameterization to pass parameters to testinfra tests

```python
import pytest
@pytest.mark.parametrize("name", ["curl", "git"])
def test_utilities_installed(host, name):
    assert host.package(name).is_installed
```

# Conclusion

Testinfra belongs in your platform toolbox.  It gives you fast, reliable feedback that a box (or image/pod) is configured the way you *think* it is—before a rollout, after a change, and during drift audits. It scales from “does nginx listen on 80” to “is our org-wide security baseline present, enabled, and locked down,” and because it’s pytest, it integrates well into your workflow and CI with minimal effort.
