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.
Table of contents
Install the testaid plugin using pip:
$ pip install testaid
Run unit tests (pytest
) and system tests (molecule test
) by invoking tox:
$ tox
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.
As a boilerplate for testinfra tests it is enough to do:
import testaid
testinfra_hosts = testaid.hosts()
You can access gopass secrets by using the testpass fixture:
def test_mytest(host, testpass):
my_password = testpass('my_project/my_password')
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.
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).
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 against the molecule host to gathers ansible variables and facts. It then runs a playbook against localhost 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-gatherfrom-moleculehost
Do not gather variables from molecule host. Read variables directly from disk without running a playbook. It's faster but there is no inventory involved so e.g. groups won't work.
testvars-no-resolve-vars
Do not resolve any jinja2 template. This option might speed up some (unit) tests. Implies
testvars-no-gather-facts
,testvars-no-gather-molecule
andtestvars-no-extra-vars
.
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.
testvars-no-resolvevia-localhost
Do not resolve variables against localhost. Resolve against molecule host instead. This option is only a fallback in case of unknown bugs.
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
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