pytest-testinfra icon indicating copy to clipboard operation
pytest-testinfra copied to clipboard

Documentation on creating custom TestInfra Module

Open filbotblue opened this issue 3 years ago • 5 comments

Details

I am wanting to write a custom TestInfra module for something that isn't provided by TestInfra by default. So, I wanted to test this out by doing something trivial. So, I looked at the testinfra/modules/podman.py file and thought I had identified how this should work, but I am not doing it correctly. Here is what my module is doing.

"""TestInfra Plugin Module
"""

from testinfra.modules.base import Module


class Myname(Module):
    """Myname Class. It inherits the TestInfra Module class.
    """

    def __init__(self, name):
        """Class Constructor
        """
        self.name = name
        super().__init__()

    @property
    def is_dudley(self):
        """Verify that the string is Dudley
        """
        string: str = "Dudley"

        return string

I then used Poetry to install the module. Then I use the following test file:

"""Test the TestInfra Module
"""


def test_is_dudley(host):
    """Test if is_dudley returns Dudley.
    """
    dudley = host.myname("phillip")
    assert dudley.is_dudley == "Dudley"

Then I ran the following command to attempt to run the module.

poetry run py.test

Then I got the following error.

➜  pytest-testinfra-dudley
> poetry run py.test
================ test session starts ================
platform linux -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: /var/home/filbot/bluekc/development/pytest-testinfra-dudley
plugins: testinfra-6.8.0
collected 1 item

tests/test_pytest_testinfra_dudley.py F                                                                                                                                                                       [100%]

=============== FAILURES ========================
_______________ test_is_dudley[local] ____________________

host = <testinfra.host.Host local>

    def test_is_dudley(host):
        """Test if is_dudley returns Dudley.
        """
>       dudley = host.myname("phillip")

tests/test_pytest_testinfra_dudley.py:8:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <testinfra.host.Host local>, name = 'myname'

    def __getattr__(self, name):
        if name in testinfra.modules.modules:
            module_class = testinfra.modules.get_module_class(name)
            obj = module_class.get_module(self)
            setattr(self, name, obj)
            return obj
>       raise AttributeError(
            "'{}' object has no attribute '{}'".format(self.__class__.__name__, name)
        )
E       AttributeError: 'Host' object has no attribute 'myname'

../../../.cache/pypoetry/virtualenvs/pytest-testinfra-dudley-djwyNVfS-py3.10/lib/python3.10/site-packages/testinfra/host.py:120: AttributeError
=============== short test summary info ==============
FAILED tests/test_pytest_testinfra_dudley.py::test_is_dudley[local] - AttributeError: 'Host' object has no attribute 'myname'
=============== 1 failed in 0.03s =================

Question

Is there any documentation on how to extend TestInfra with a custom module plugin? I'm not doing something correctly.

filbotblue avatar Aug 22 '22 21:08 filbotblue

I also created a Reddit post asking for any pointers as well.

FilBot3 avatar Oct 24 '22 18:10 FilBot3

Moin,

I've been looking at this because I needed something similar. The main blocker for getting modules from other python modules loaded is in the testinfra.modules.get_modules method:

https://github.com/pytest-dev/pytest-testinfra/blob/dc48cd98f3b1970ecbad63f866a94f8283b79ce4/testinfra/modules/init.py#L48-L52

This pretty much hardcodes all modules to live in python modules testinfra.modules.foo.

Changing this wouldn't be such a massive change. I've done so in a branch on a fork. I mostly split the builtin modules into a separate object and added a decorator to register modules, so external code only needs to care about the decorator:

https://github.com/Serviceware/pytest-testinfra/blob/ecd50844024f00a4fcf83b2a360fbaf220c91a4a/testinfra/modules/init.py#L47-L57

https://github.com/Serviceware/pytest-testinfra/blob/ecd50844024f00a4fcf83b2a360fbaf220c91a4a/testinfra/modules/base.py#L17-L39

With this, I can setup a new module like this (just using firewalld as an example, as that's currently on my mind, and I haven't really implemented any proper parsing yet):

from testinfra.modules.base import Module, register_module


@register_module("firewalld_zone")
class FirewalldZone(Module):
    def __init__(self, zone):
        self.zone = zone
        super().__init__()

    def info(self):
        return self.check_output("firewall-cmd --info-zone {}".format(self.zone))
def test_foo(host):
    zone = host.firewalld_zone("public")
    ...

This should be fine from a security perspective, because I have to import the module anyhow to execute the module registration.

I'm mostly concerned about introducing public APIs, but if the maintainers are fine with this, I could finish this up with some documentation and open a PR. Tests are running cleanly on the branch for me.

Tetha avatar Nov 20 '23 14:11 Tetha

I was looking for this feature. It would be nice to be able to extend the testinfra with custom modules without modifying the package.

snikiten-harmonicinc avatar Apr 12 '24 18:04 snikiten-harmonicinc

@philpep sorry for the ping, but can I call your attention to this issue? I think we even have a way to address it thanks to @Tetha and since this was opened back in 2022, it would be awesome to find a solution.

pecigonzalo avatar Apr 26 '24 09:04 pecigonzalo