Package: pytest-ansible
pytest-ansibleThepytest-ansibleplugin is designed to provide seamless integration betweenpytestandAnsible, allowing you to efficiently run and test Ansible-related
tasks and scenarios within your pytest test suite. This plugin enhances the
testing workflow by offering three distinct pieces of functionality:Unit Testing for Ansible Collections: This feature aids in running unit
tests forAnsible collectionsusingpytest. It allows you to validate the
behavior of your Ansiblemodulesandrolesin isolation, ensuring that
each component functions as expected.Molecule Scenario Integration: The plugin assists in running Moleculescenariosusingpytest. This integration streamlines the testing of
Ansible roles and playbooks across different environments, making it easier
to identify and fix issues across diverse setups.Ansible Integration for Pytest Tests: With this functionality, you can
seamlessly useAnsiblefrom within yourpytesttests. This opens up
possibilities to interact with Ansible components and perform tasks like
provisioning resources, testing configurations, and more, all while
leveraging the power and flexibility of pytest.Supported AnsiblePytest Ansible will only support versions ofpythonandansible-corewhich are underactive upstream supportwhich currently translates to:Python 3.10 or newerAnsible-core 2.14 or newerInstallationInstall this plugin usingpip:pip install pytest-ansibleGetting StartedUnit Testing for Ansible CollectionsThepytest-ansible-unitsplugin allows ansible collection's unit tests to be
run with onlypytest. It offers a focused approach to testing individual
Ansible modules. With this plugin, you can write and execute unit tests
specifically for Ansible modules, ensuring the accuracy and reliability of your
module code. This is particularly useful for verifying the correctness of module
behavior in isolation.To usepytest-ansible-units, follow these steps:Install the plugin using pip:pip install pytest-ansibleEnsure you have Python 3.10 or greater, ansible-core, and pyyaml installed.Depending on your preferred directory structure, you can clone collections
into the appropriate paths.Collection Tree Approach: The preferred approach is to clone the
collections being developed into it's proper collection tree path. This
eliminates the need for any symlinks and other collections being developed
can be cloned into the same tree structure.git clone <repo> collections/ansible_collections/<namespace>/<name>Note: Runpytestin the root of the collection directory, adjacent to the
collection'sgalaxy.ymlfile.Shallow Tree Approach:git clone <repo>Notes:Runpytestin the root of the collection directory, adjacent to the
collection'sgalaxy.ymlfile.A collections directory will be created in the repository directory, and
collection content will be linked into it.Execute the unit tests using pytest:pytest testsHelpThe following may be added to the collections'pyproject.tomlfile to limit
warnings and set the default path for the collection's tests[tool.pytest.ini_options]
testpaths = [
    "tests",
]
filterwarnings = [
    'ignore:AnsibleCollectionFinder has already been configured',
]Information from thegalaxy.ymlfile is used to build thecollectionsdirectory structure and link the contents. Thegalaxy.ymlfile should reflect
the correct collection namespace and name.One way to detect issues without running the tests is to run:pytest --collect-onlyThe follow errors may be seen:E   ModuleNotFoundError: No module named 'ansible_collections'Check thegalaxy.ymlfile for an accurate namespace and nameEnsurepytestis being run from the collection's root directory, adjacent to
thegalaxy.ymlHINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modulesMolecule Scenario IntegrationThis functionality assists in running Moleculescenariosusingpytest. It
enables pytest discovery of allmolecule.ymlfiles inside the codebase and
runs them as pytest tests. It allows you to include Molecule scenarios as part
of your pytest test suite, allowing you to thoroughly test your Ansible roles
and playbooks across different scenarios and environments.Running molecule scenarios using pytestMolecule scenarios can be tested using 2 different methods if molecule is
installed.Recommended:Add atest_integration.pyfile to thetests/integrationdirectory of the
ansible collection:"""Tests for molecule scenarios."""
from __future__ import absolute_import, division, print_function

from pytest_ansible.molecule import MoleculeScenario


def test_integration(molecule_scenario: MoleculeScenario) -> None:
    """Run molecule for each scenario.

    :param molecule_scenario: The molecule scenario object
    """
    proc = molecule_scenario.test()
    assert proc.returncode == 0Themolecule_scenariofixture provides parameterized molecule scenarios
discovered in the collection'sextensions/moleculedirectory, as well as other
directories within the collection.molecule test -s <scenario>will be run for each scenario and a completed
subprocess returned from thetest()call.Legacy:Run molecule with the--moleculecommand line parameter to inject each
molecule directory found in the current working directory. Each scenario will be
injected as an external test in the the tests available for pytest. Due to the
nature of this approach, the molecule scenarios are not represented as python
tests and may not show in the IDE's pytest test tree.To run Molecule scenarios using pytest, follow these steps:Install thepytest-ansibleplugin using pip:pip install pytest-ansibleExecute pytest to run Molecule scenarios:pytestAnsible Integration for Pytest TestsTheansible_module,ansible_adhoc,localhost, andansible_factsfixtures
are provided to help you integrate Ansible functionalities into your pytest
tests. These fixtures allow you to interact with Ansible modules, run commands
on localhost, fetch Ansible facts, and more.Fixtures and helpers for use in testsHere's a quick overview of the available fixtures:ansible_module: Allows you to call Ansible modules directly within your test
functions.ansible_adhoc: Provides a function to initialize aHostManagerobject to
work with Ansible inventory.localhost: A convenience fixture for running Ansible modules that typically
run on the local machine.ansible_facts: Returns a JSON structure representing system facts for the
associated inventory.UsageOnce installed, the followingpytestcommand-line parameters are available:pytest\[--inventory<path_to_inventory>]\[--extra-inventory<path_to_extra_inventory>]\[--host-pattern<host-pattern>]\[--connection<plugin>]\[--module-path<path_to_modules]\[--user<username>]\[--become]\[--become-user<username>]\[--become-method<method>]\[--ask-become-pass]\[--limit<limit>]\[--ansible-unit-inject-only]\[--molecule]\[--molecule-unavailable-driver]\[--skip-no-git-change]\[--check]InventoryUsing ansible first starts with defining your inventory. This can be done in
several ways, but to start, we'll use theansible_adhocfixture.deftest_my_inventory(ansible_adhoc):hosts=ansible_adhoc()In the example above, thehostsvariable is an instance of theHostManagerclass and describes your ansible inventory. For this to work, you'll need to
tellansiblewhere to find your inventory. Inventory can be anything supported
by ansible, which includes anINI fileor an
executable script that returnsproperly formatted JSON.
For example,pytest--inventorymy_inventory.ini--host-patternallorpytest--inventorypath/to/my/script.py--host-patternwebserversorpytest--inventoryone.example.com,two.example.com--host-patternallIn the above examples, the inventory provided at runtime will be used in all
tests that use theansible_adhocfixture. A more realistic scenario may
involve using different inventory files (or host patterns) with different tests.
To accomplish this, the fixtureansible_adhocallows you to customize the
inventory parameters. Read on for more detail on using theansible_adhocfixture.Extra InventoryUsing ansible first starts with defining your extra inventory. This feature was
added in version 2.3.0, and is intended to allow the user to work with two
different inventories. This can be done in several ways, but to start, we'll use
theansible_adhocfixture.For example,pytest--inventorymy_inventory.ini--extra-inventorymy_second_inventory.ini--host-patternhost_in_second_inventoryFixtureansible_adhocTheansible_adhocfixture returns a function used to initialize aHostManagerobject. Theansible_adhocfixture will default to parameters
supplied to thepytestcommand-line, but also allows one to provide keyword
arguments used to initialize the inventory.The example below demonstrates basic usage with options supplied at run-time topytest.deftest_all_the_pings(ansible_adhoc):ansible_adhoc().all.ping()The following example demonstrates available keyword arguments when creating aHostManagerobject.deftest_uptime(ansible_adhoc):# take down the databaseansible_adhoc(inventory='db1.example.com,',user='ec2-user',become=True,become_user='root').all.command('reboot')TheHostManagerobject returned by theansible_adhoc()function provides
numerous ways of calling ansible modules against some, or all, of the inventory.
The following demonstrates sample usage.deftest_host_manager(ansible_adhoc):hosts=ansible_adhoc()# __getitem__hosts['all'].ping()hosts['localhost'].ping()# __getattr__hosts.all.ping()hosts.localhost.ping()# Supports [ansible host patterns](http://docs.ansible.com/ansible/latest/intro_patterns.html)hosts['webservers:!phoenix'].ping()# all webservers that are not in phoenixhosts[0].ping()hosts[0:2].ping()assert'one.example.com'inhostsasserthasattr(hosts,'two.example.com')fora_hostinhosts:a_host.ping()FixturelocalhostThelocalhostfixture is a convenience fixture that surfaces aModuleDispatcherinstance for ansible host runningpytest. This is
convenient when using ansible modules that typically run on the local machine,
such as cloud modules (ec2, gce etc...).deftest_do_something_cloudy(localhost,ansible_adhoc):"""Deploy an ec2 instance using multiple fixtures."""params=dict(key_name='some_key',instance_type='t2.micro',image='ami-123456',wait=True,group='webservers',count=1,vpc_subnet_id='subnet-29e63245',assign_public_ip=True,)# Deploy an ec2 instance from localhost using the `ansible_adhoc` fixtureansible_adhoc(inventory='localhost,',connection='local').localhost.ec2(**params)# Deploy an ec2 instance from localhost using the `localhost` fixturelocalhost.ec2(**params)Fixtureansible_moduleTheansible_modulefixture allows tests and fixtures to callansible modules. Unlike theansible_adhocfixture, this fixture only uses the options supplied topytestat run time.A very basic example demonstrating the ansiblepingmodule:deftest_ping(ansible_module):ansible_module.ping()A more involved example of updating the sshd configuration, and restarting the
service.deftest_sshd_config(ansible_module):# update sshd MaxSessionscontacted=ansible_module.lineinfile(dest="/etc/ssh/sshd_config",regexp="^#?MaxSessions .*",line="MaxSessions 150"))# assert desired outcomefor(host,result)incontacted.items():assert'failed'notinresult,result['msg']assert'changed'inresult# restart sshdcontacted=ansible_module.service(name="sshd",state="restarted")# assert successful restartfor(host,result)incontacted.items():assert'changed'inresultandresult['changed']assertresult['name']=='sshd'# do other stuff ...Fixtureansible_factsTheansible_factsfixture returns a JSON structure representing the system
facts for the associated inventory. Sample fact data is available in theansible documentation.Note, this fixture is provided for convenience and could easily be called usingansible_module.setup().A systems facts can be useful when deciding whether to skip a test ...deftest_something_with_amazon_ec2(ansible_facts):forfactsinansible_facts:if'ec2.internal'!=facts['ansible_domain']:pytest.skip("This test only applies to ec2 instances")Additionally, since facts are just ansible modules, you could inspect the
contents of theec2_factsmodule for greater granularity ...deftest_terminate_us_east_1_instances(ansible_adhoc):forfactsinansible_adhoc().all.ec2_facts():iffacts['ansible_ec2_placement_region'].startswith('us-east'):'''do some testing'''Parameterize withpytest.mark.ansiblePerhaps the--ansible-inventory=<inventory>includes many systems, but you
only wish to interact with a subset. Thepytest.mark.ansiblemarker can be
used to modify thepytest-ansiblecommand-line parameters for a single test.
Please note, the fixtureansible_adhocis the prefer mechanism for interacting
with ansible inventory within tests.For example, to interact with the local system, you would adjust thehost_patternandconnectionparameters.@pytest.mark.ansible(host_pattern='local,',connection='local')deftest_copy_local(ansible_module):# create a file with random datacontacted=ansible_module.copy(dest='/etc/motd',content='PyTest is amazing!',owner='root',group='root',mode='0644',)# assert only a single host was contactedassertlen(contacted)==1,\"Unexpected number of hosts contacted (%d!=%d)"%\(1,len(contacted))assert'local'incontacted# assert the copy module reported changesassert'changed'incontacted['local']assertcontacted['local']['changed']Note, the parameters provided bypytest.mark.ansiblewill apply to all class
methods.@pytest.mark.ansible(host_pattern='local,',connection='local')classTest_Local(object):deftest_install(self,ansible_module):'''do some testing'''deftest_template(self,ansible_module):'''do some testing'''deftest_service(self,ansible_module):'''do some testing'''Inspecting resultsWhen using theansible_adhoc,localhostoransible_modulefixtures, the
object returned will be an instance of classAdHocResult. TheAdHocResultclass can be inspected as follows:deftest_adhoc_result(ansible_adhoc):contacted=ansible_adhoc(inventory=my_inventory).command("date")# As a dictionaryfor(host,result)incontacted.items():assertresult.is_successful,"Failed on host%s"%hostforresultincontacted.values():assertresult.is_successfulforhostincontacted.keys():asserthostin['localhost','one.example.com']assertcontacted.localhost.is_successful# As a listassertlen(contacted)>0assert'localhost'incontacted# As an iteratorforresultincontacted:assertresult.is_successful# With __getattr__assertcontacted.localhost.is_successful# Or __getitem__assertcontacted['localhost'].is_successfulUsing theAdHocResultobject provides ways to conveniently access results for
different hosts involved in the ansible adhoc command. Once the specific host
result is found, you may inspect the result of the ansible adhoc command on that
use by way of theModuleResultinterface. TheModuleResultclass represents
the dictionary returned by the ansible module for a particular host. The
contents of the dictionary depend on the module called.TheModuleResultinterface provides some convenient properties to determine
the success of the module call. Examples are included below.deftest_module_result(localhost):contacted=localhost.command("find /tmp")assertcontacted.localhost.is_successfulassertcontacted.localhost.is_okassertcontacted.localhost.is_changedassertnotcontacted.localhost.is_failedcontacted=localhost.shell("exit 1")assertcontacted.localhost.is_failedassertnotcontacted.localhost.is_successfulThe contents of the JSON returned by an ansible module differs from module to
module. For guidance, consult the documentation and examples for the specificansible module.Exception handlingIfansibleis unable to connect to any inventory, an exception will be raised.@pytest.mark.ansible(inventory='unreachable.example.com,')deftest_shutdown(ansible_module):# attempt to ping a host that is down (or doesn't exist)pytest.raises(pytest_ansible.AnsibleHostUnreachable):ansible_module.ping()Sometimes, only a single host is unreachable, and others will have properly
returned data. The following demonstrates how to catch the exception, and
inspect the results.@pytest.mark.ansible(inventory='good:bad')deftest_inventory_unreachable(ansible_module):exc_info=pytest.raises(pytest_ansible.AnsibleHostUnreachable,ansible_module.ping)(contacted,dark)=exc_info.value.results# inspect the JSON result...for(host,result)incontacted.items():assertresult['ping']=='pong'for(host,result)indark.items():assertresult['failed']==TrueContributingContributions are very welcome. Tests can be run withtox, please ensure the coverage at least stays
the same before you submit a pull request.LicenseDistributed under the terms of theMITlicense, "pytest-ansible" is free and open source softwareIssuesIf you encounter any problems, pleasefile an issuealong with
a detailed description.