Unit Testing in Ansible
Overview
Unit testing in Ansible focuses on testing individual components like custom modules, filters, and plugins. This ensures each component works correctly in isolation before being integrated into larger playbooks.
Testing Custom Modules
Directory Structure
custom_module/
├── library/
│ └── my_custom_module.py
└── tests/
└── unit/
├── test_my_custom_module.py
└── conftest.py
Example Custom Module
# library/my_custom_module.py
from ansible.module_utils.basic import AnsibleModule
def run_module():
module_args = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent']),
value=dict(type='str', required=False)
)
result = dict(
changed=False,
original_message='',
message=''
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Module logic here
if module.params['state'] == 'present':
result['changed'] = True
result['message'] = f"Created {module.params['name']}"
else:
result['message'] = f"Removed {module.params['name']}"
module.exit_json(**result)
if __name__ == '__main__':
run_module()
Unit Test Example
# tests/unit/test_my_custom_module.py
import pytest
from ansible.module_utils import basic
from ansible.module_utils.common.text.converters import to_bytes
import json
def set_module_args(args):
"""Prepare arguments so that they will be picked up during module creation"""
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
class TestMyCustomModule:
def test_module_create(self, capfd):
"""Test if module creates resource correctly"""
set_module_args({
'name': 'test_resource',
'state': 'present'
})
with pytest.raises(SystemExit):
import my_custom_module
out, err = capfd.readouterr()
result = json.loads(out)
assert result['changed']
assert "Created test_resource" in result['message']
def test_module_remove(self, capfd):
"""Test if module removes resource correctly"""
set_module_args({
'name': 'test_resource',
'state': 'absent'
})
with pytest.raises(SystemExit):
import my_custom_module
out, err = capfd.readouterr()
result = json.loads(out)
assert not result['changed']
assert "Removed test_resource" in result['message']
Testing Filters and Plugins
Custom Filter Example
# filter_plugins/custom_filters.py
class FilterModule(object):
def filters(self):
return {
'format_hostname': self.format_hostname
}
def format_hostname(self, hostname, domain=None):
if domain:
return f"{hostname}.{domain}"
return hostname
Filter Unit Test
# tests/unit/test_filters.py
import pytest
from filter_plugins.custom_filters import FilterModule
class TestCustomFilters:
def setup_method(self):
self.filters = FilterModule().filters()
def test_format_hostname(self):
assert self.filters['format_hostname']('web01', 'example.com') == 'web01.example.com'
assert self.filters['format_hostname']('web01') == 'web01'
Running Unit Tests
# Install testing requirements
pip install pytest pytest-mock ansible
# Run tests with pytest
pytest tests/unit/ -v
# Run with coverage
pytest tests/unit/ --cov=library --cov=filter_plugins
Best Practices for Unit Testing
- Test Organization
- Group related tests in classes
- Use meaningful test names
-
Follow the Arrange-Act-Assert pattern
-
Mock External Dependencies
python def test_with_mock(mocker): mock_cmd = mocker.patch('subprocess.run') mock_cmd.return_value.returncode = 0 # Test code that uses subprocess.run
-
Test Edge Cases
python def test_edge_cases(self): with pytest.raises(ValueError): self.module.process_input(None) with pytest.raises(TypeError): self.module.process_input(123)
-
Parameterized Testing
python @pytest.mark.parametrize("input,expected", [ ("test1", "TEST1"), ("test2", "TEST2"), ("", ""), ]) def test_multiple_cases(self, input, expected): assert self.module.transform(input) == expected