This post is about testing custom ansible library code. Specifically the tests I wrote while working on ansible-rails, a wrapper utility for bundler and rake useful when deploying rails applications with ansible.
Sadly the documentation on how to test custom library code is really thin. Hopefully this will change but for now, here’s how I write and test custom libraries using a very simplistic example.
Most ansible libaries will look somewhat like this:
# inside library/magic
#!/usr/bin/python
# -*- coding: utf-8 -*-
DOCUMENTATION = '''some documentation'''
EXAMPLES = '''some usage examples'''
import os
# more imports
def do_magic(module):
return { 'changed': True, 'option_was': module.params.get('an_option', None) }
def main():
module = AnsibleModule(
argument_Spec = dict(
an_option = dict(required=False, type='str'),
)
)
result = do_magic(module)
module.exit_json(**result)
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
To prepare our do_magic
method to be easily testable I like to move it into a class which accepts the ansible module in the constructor. That way I can inject a fake ansible module during my tests easily:
class MagicModule(object):
module = None
def __init__(self, module):
self.module = module
# more methods here
def do_magic(self):
return { 'changed': True, 'option_was': self.module.params.get('an_option', None) }
def main():
module = AnsibleModule(
argument_Spec = dict(
an_option = dict(required=False, type='str'),
)
)
magic_module = MagicModule(module)
result = magic_module.do_magic(module)
module.exit_json(**result)
Next off we need to stop executing ansible every time we load our file. We do that by guarding the call to main
with a conditional:
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
if __name__ == '__main__':
main()
Now we can start writing a simple unit test. There’s only one problem I’ve stumbled over: ansible seems to infer the library name from the filename.
This is a problem because inside your playbook you’d rather write magic: an_option="what to ask?"
than
magic.py: an_option="what to ask?"
.
Now removing the file extension breaks pythons import functionality, and we need to work around this by using imp:
# inside test/magic_test.py
# -*- coding: utf-8 -*-
import unittest
import imp
imp.load_source('magic', os.path.join(os.path.dirname(__file__), os.path.pardir, 'library','magic'))
from magic import MagicModule
class FakeAnsibleModule(object):
check_mode = False
params = {}
class TestBase(unittest.TestCase):
def test_do_magic(self):
fake_ansible = FakeAnsibleModule()
fake_ansible.params['an_option'] = 'Hello World'
magic_module = MagicModule(fake_ansible)
result = magic_module.do_magic()
assert result['changed'] == True
assert result['option_was'] == 'Hello World'
Running the test also requires a changed PYTHONPATH to work:
PYTHONPATH=$PWD/library python test/magic_test.py
While this is not perfect I’d rather test my custom library code than leaving it untested.
That’s about all. To wrap up:
- wrap your logic inside a class, accepting AnsibleModule as constructor argument for easy dependency injection.
- load the module using imp
- test the logic and commands sent using mock or similar.