pytest-testinfra
pytest-testinfra copied to clipboard
Documentation on creating custom TestInfra Module
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.
I also created a Reddit post asking for any pointers as well.
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.
I was looking for this feature. It would be nice to be able to extend the testinfra with custom modules without modifying the package.
@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.