testaid fixtures for ansible, molecule and testinfra
About
With the pytest plugin testinfra you can write unit tests in python to test your servers configured by the management tool ansible. testinfra is the default verifier of the molecule testing environment.
The pytest plugin testaid provides helper functions and fixtures to facilitate the use of testinfra. It helps to not only unit test your ansible roles but to integration and system test your whole ansible project.
testinfra wraps cli calls to the ansible executable. testaid uses the ansible python api to run ansible playbooks.
Outline
Table of contents
License
Quick start
Install the testaid plugin using pip:
$ pip install testaid
Tests
Run unit tests (pytest
) and system tests (molecule test
) by invoking tox:
$ tox
Example
Have a look at debian system test directory for an example of a molecule project using ansible, testinfra and testaid. The molecule project doubles as as a system test (golden master) for the testaid plugin.
Boilerplate
As a boilerplate for testinfra tests it is enough to do:
import testaid
testinfra_hosts = testaid.hosts()
Fixture testpass
You can access gopass secrets by using the testpass fixture:
def test_mytest(host, testpass):
my_password = testpass('my_project/my_password')
Fixture testvars
Arguably the most useful feature of the testaid plugin is the testvars fixture. The fixture resolves and exposes ansible variables as a python dict:
def test_mytest(host, testvars):
my_variable = testvars['my_variable']
testvars runs a playbook against the molecule host using the ansible python api.
testvars creates a symbolic link to the roles directory of your ansible project
in the ephemeral playbook environment which molecule sets up.
It then runs a playbook with gather_facts:true
and a debug task to get
the ansible variables and the ansible facts of the play and host.
testvars uses the ansible VariableManager so the usual ansible variable precedence rules apply. Internally, the fixture uses the ansible debug module to resolve templates. Thus, it can resolve any kind of template that the debug module can resolve including jinja2 code and calls to lookup plugins.
extra vars
The TESTVARS_EXTRA_VARS
environment variable can be set in molecule.yml.
It can contain dirpaths or filepaths relative to the
MOLECULE_SCENARIO_DIRECTORY
separated by colons:
verifier:
name: testinfra
env:
TESTVARS_EXTRA_VARS: "../../vars:../../extra_vars/extra_vars.yml"
The vars files will be included in moleculebook playbooks by adding
the paths to vars_files
(and not by adding include_vars
tasks).
roles
Which roles are included is determined in this order:
- List of roles separated by colon specified in the
TESTVARS_ROLES_WHITELIST
environment variable - List of roles specified in playbook speciied in
molecule.yml
- List of roles specified in default playbook
playbook.yml
- All roles in
roles
directory in project directory
Roles blacklisted in TESTVARS_ROLES_BLACKLIST
won't be included.
options
testvars is a session scope fixture so its configuration is done in molecule.yml by using pytest command line options. You can add a couple of options in the options dictionary of the verifier section:
verifier:
name: testinfra
options:
testvars-no-gather-facts: true
By default, testvars runs a playbook to gather ansible variables and facts. It then runs a playbook to resolve the variables.
You can change the default behaviour with these options:
-
testvars-no-gather-facts
- Run playbook to gather variables with
gather_facts: false
. You won't be able to accessansible_facts
but your tests will run faster.
-
testvars-no-gather-molecule
- Do not resolve molecule variables. You probably won't need these variables but it won't take much time to resolve them, either.
-
testvars-no-extra-vars
- Do not add extra variables specified in
TESTVARS_EXTRA_VARS
. Ignores the environment variable.
caching
Hopefully the testvars fixture allows fast test-driven development. It has session scope so variables are collected and resolved only once per testrun as pytest caches the result. If this is still too slow for you then you can enable the pytest cache plugin in molecule.yml:
verifier:
name: testinfra
options:
p: cacheprovider
You should use the testaid boilerplate code to be able to run pytest directly. Otherwise testinfra will complain about missing environment variables.
Remember to clear the cache when you add or change an ansible variable:
pytest --cache-clear; molecule verify
The cache will use the molecule ephemeral directory as the cache key which is unique for each molecule instance. When using the boilerplate you can inspect the cache by running:
pytest --cache-show
Ansible Python API
The testaid plugin provides four main pytest fixtures (and a couple of command line, environment variables and helper fixtures):
- testpass - exposes the ansible passwordstore plugin
- testvars - resolves and exposes ansible vars and facts
- moleculebook - api to run playbooks against a molecule host
- moleculeplay - api to leverage the ansible python api
The testvars and testpass fixtures use the moleculebook fixture which in turn uses the moleculeplay fixture. moleculeplay makes low-level calls to the ansible python api and uses the moleculeenv fixture to handle the sysadmin tasks of setting the right symlinks. moleculeplay and moleculeenv will probably not be very useful on their own but moleculebook might be handy in those situations where you know you shouldn't implement a hackaround. ;-)
Here is how you could run an ansible playbook programmatically from a test (or even better: from a fixture) using dependency injection.
def test_testaid_moleculebook(host, moleculebook):
playbook = moleculebook.get()
args = dict(path='/tmp/moleculebook_did_this', state='touch')
task_touch = dict(action=dict(module='file', args=args))
playbook['tasks'].append(task_touch)
moleculebook.set(playbook)
moleculebook.run()
assert host.file('/tmp/moleculebook_did_this').exists