I've made the difficult decision to deprecate myself.
My first commit was 25 July 2014 — a small pytest plugin so you could aim Ansible at inventory from inside a test function and get something friendlier than shell soup. Over the next decade-plus (700+ commits) I grew inventory fixtures, markers, collection unit-test path injection, and for a while even helped run Molecule scenarios through pytest. It was a good run.
Somewhere along the way the ecosystem caught up — and then leaped ahead. Plain
pytest, ansible-test, molecule (especially with shared_state and
--workers), tox-ansible, ansible-dev-environment (ADE), and
ansible-creator now carry the day for collection authors. They own
orchestration, installs, and scenarios better than a single plugin ever should.
That's not a eulogy for pytest; it's a thank-you to the rich Python testing
world that made better tools possible.
To everyone who filed issues, sent PRs, reviewed docs, fixed CI at odd hours, or quietly kept dependencies green: thank you. James Laska lit the fuse; many humans (and a few tireless bots) kept the lights on. You're why I lasted long enough to see myself replaced properly.
Planned project end-of-life: December 2026. Until then I still install and run for existing users. Prefer pinning a release if you need more time. Molecule-via-pytest is already on a faster deprecation track — see Molecule Scenario Integration (deprecated) below.
| What you used me for | Use instead |
|---|---|
| Collection unit tests / import path magic | ADE (ade install -e) + plain pytest |
| Collection CI matrix | tox-ansible |
| Integration / Molecule scenarios | molecule test --all (and tox-ansible's molecule test type) |
| Iterate hosts/groups in pytest | ansible-inventory --list + pytest.mark.parametrize (below) |
| Ad-hoc module calls from pytest | Small in-repo helper shelling out to ansible (below), or a Molecule scenario |
# tests/conftest.py — copy into your repo
from __future__ import annotations
import json
import subprocess
def inventory_hosts(inventory: str = "inventory") -> list[str]:
data = json.loads(
subprocess.check_output(
["ansible-inventory", "-i", inventory, "--list"],
text=True,
)
)
hosts = data.get("_meta", {}).get("hostvars") or data.get("all", {}).get("hosts") or []
return sorted(hosts)
def pytest_generate_tests(metafunc) -> None:
if "hostname" in metafunc.fixturenames:
metafunc.parametrize("hostname", inventory_hosts())# tests/test_ping.py
import subprocess
def test_ping(hostname: str) -> None:
subprocess.run(
["ansible", "-i", "inventory", hostname, "-m", "ping"],
check=True,
)For a group, read data["webservers"]["hosts"] (or your group name) from the
same JSON and parametrize that list instead.
# tests/helpers/ansible_cli.py
from __future__ import annotations
import subprocess
def run_module(
module: str,
*,
inventory: str = "inventory",
hosts: str = "all",
args: str = "",
extra: list[str] | None = None,
) -> subprocess.CompletedProcess[str]:
cmd = ["ansible", "-i", inventory, hosts, "-m", module]
if args:
cmd.extend(["-a", args])
if extra:
cmd.extend(extra)
return subprocess.run(cmd, check=False, capture_output=True, text=True)def test_setup_localhost() -> None:
proc = run_module(
"setup",
inventory="localhost,",
hosts="localhost",
extra=["-c", "local"],
)
assert proc.returncode == 0
def test_become_example(hostname: str) -> None:
proc = run_module(
"ping",
hosts=hostname,
extra=["--become", "--become-user", "root"],
)
assert proc.returncode == 0These patterns replace ansible_host / ansible_group iterators and most
ansible_module / ansible_adhoc / ansible_facts / localhost usage. They
are deliberately copy-paste — not a new shared library. For real multi-host
integration with shared infrastructure, prefer Molecule.
Until December 2026, the fixtures and CLI flags below remain available but are not the recommended path. Prefer the migration notes above.
Pytest Ansible will only support versions of python and ansible-core which are under active upstream support which currently translates to:
- Python 3.10 or newer
- Ansible-core 2.14 or newer
Install this plugin using pip:
pip install pytest-ansible
The pytest-ansible-units plugin allows ansible collection's unit tests to be
run with only pytest. 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 use pytest-ansible-units, follow these steps:
- Install the plugin using pip:
pip install pytest-ansible
-
Ensure 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: Run
pytestin the root of the collection directory, adjacent to the collection'sgalaxy.ymlfile. -
Shallow Tree Approach:
git clone <repo>Notes:
- Run
pytestin 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.
- Run
-
-
Execute the unit tests using pytest:
pytest tests
The following may be added to the collections' pyproject.toml file 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 the galaxy.yml file is used to build the collections
directory structure and link the contents. The galaxy.yml file should reflect
the correct collection namespace and name.
One way to detect issues without running the tests is to run:
pytest --collect-only
The follow errors may be seen:
E ModuleNotFoundError: No module named 'ansible_collections'
- Check the
galaxy.ymlfile for an accurate namespace and name - Ensure
pytestis being run from the collection's root directory, adjacent to thegalaxy.yml
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
Deprecated: Running Molecule scenarios via pytest-ansible will be removed in a future release.
Prefer the Molecule CLI (and tox-ansible for collections):
molecule test --all
# With shared_state collections, use molecule's own concurrency:
molecule test --all --workers 4pytest-ansible runs each scenario as a separate molecule test -s <scenario>
subprocess. That only works for standalone scenarios. It does not
support Molecule shared_state (create once in the default scenario, run
component scenarios, destroy once) or --workers. Those require Molecule's
native multi-scenario orchestration.
| Old (pytest-ansible) | New |
|---|---|
molecule_scenario fixture + glue test file |
molecule test --all |
pytest --molecule |
molecule test --all |
| Collection CI via pytest integration env | tox-ansible molecule test type |
The --molecule* CLI options, molecule_scenario fixture, and
pytest_ansible.molecule.MoleculeScenario remain available temporarily but
emit a DeprecationWarning.
Legacy: Prefer the migration notes above. These fixtures remain until the December 2026 project end-of-life.
The ansible_module, ansible_adhoc, localhost, and ansible_facts fixtures
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.
Here'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.
Once installed, the following pytest command-line parameters are available:
pytest \
[--inventory <path_to_inventory>] \
[--extra-inventory <path_to_extra_inventory>] \
[--host-pattern <host-pattern>] \
[--ansible-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]--molecule, --molecule_unavailable_driver, and --skip_no_git_change are
deprecated; prefer molecule test --all.
Using ansible first starts with defining your inventory. This can be done in
several ways, but to start, we'll use the ansible_adhoc fixture.
def test_my_inventory(ansible_adhoc):
hosts = ansible_adhoc()In the example above, the hosts variable is an instance of the HostManager
class and describes your ansible inventory. For this to work, you'll need to
tell ansible where to find your inventory. Inventory can be anything supported
by ansible, which includes an
INI file or an
executable script that returns
properly formatted JSON.
For example,
pytest --inventory my_inventory.ini --host-pattern allor
pytest --inventory path/to/my/script.py --host-pattern webserversor
pytest --inventory one.example.com,two.example.com --host-pattern allIn the above examples, the inventory provided at runtime will be used in all
tests that use the ansible_adhoc fixture. A more realistic scenario may
involve using different inventory files (or host patterns) with different tests.
To accomplish this, the fixture ansible_adhoc allows you to customize the
inventory parameters. Read on for more detail on using the ansible_adhoc
fixture.
Using 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
the ansible_adhoc fixture.
For example,
pytest --inventory my_inventory.ini --extra-inventory my_second_inventory.ini --host-pattern host_in_second_inventoryThe ansible_adhoc fixture returns a function used to initialize a
HostManager object. The ansible_adhoc fixture will default to parameters
supplied to the pytest command-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 to
pytest.
def test_all_the_pings(ansible_adhoc):
ansible_adhoc().all.ping()The following example demonstrates available keyword arguments when creating a
HostManager object.
def test_uptime(ansible_adhoc):
# take down the database
ansible_adhoc(
inventory="db1.example.com,", user="ec2-user", become=True, become_user="root"
).all.command("reboot")The HostManager object returned by the ansible_adhoc() function provides
numerous ways of calling ansible modules against some, or all, of the inventory.
The following demonstrates sample usage.
def test_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 phoenix
hosts[0].ping()
hosts[0:2].ping()
assert "one.example.com" in hosts
assert hasattr(hosts, "two.example.com")
for a_host in hosts:
a_host.ping()The localhost fixture is a convenience fixture that surfaces a
ModuleDispatcher instance for ansible host running pytest. This is
convenient when using ansible modules that typically run on the local machine,
such as cloud modules (ec2, gce etc...).
def test_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` fixture
ansible_adhoc(inventory="localhost,", connection="local").localhost.ec2(**params)
# Deploy an ec2 instance from localhost using the `localhost` fixture
localhost.ec2(**params)The ansible_module fixture allows tests and fixtures to call
ansible modules. Unlike the
ansible_adhoc fixture, this fixture only uses the options supplied to pytest
at run time.
A very basic example demonstrating the ansible
ping module:
def test_ping(ansible_module):
ansible_module.ping()A more involved example of updating the sshd configuration, and restarting the service.
def test_sshd_config(ansible_module):
# update sshd MaxSessions
contacted = ansible_module.lineinfile(
dest="/etc/ssh/sshd_config",
regexp="^#?MaxSessions .*",
line="MaxSessions 150")
)
# assert desired outcome
for (host, result) in contacted.items():
assert 'failed' not in result, result['msg']
assert 'changed' in result
# restart sshd
contacted = ansible_module.service(
name="sshd",
state="restarted"
)
# assert successful restart
for (host, result) in contacted.items():
assert 'changed' in result and result['changed']
assert result['name'] == 'sshd'
# do other stuff ...The ansible_facts fixture returns a JSON structure representing the system
facts for the associated inventory. Sample fact data is available in the
ansible documentation.
Note, this fixture is provided for convenience and could easily be called using
ansible_module.setup().
A systems facts can be useful when deciding whether to skip a test ...
def test_something_with_amazon_ec2(ansible_facts):
for facts in ansible_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 the ec2_facts module for greater granularity ...
def test_terminate_us_east_1_instances(ansible_adhoc):
for facts in ansible_adhoc().all.ec2_facts():
if facts["ansible_ec2_placement_region"].startswith("us-east"):
"""do some testing"""Perhaps the --ansible-inventory=<inventory> includes many systems, but you
only wish to interact with a subset. The pytest.mark.ansible marker can be
used to modify the pytest-ansible command-line parameters for a single test.
Please note, the fixture ansible_adhoc is the prefer mechanism for interacting
with ansible inventory within tests.
For example, to interact with the local system, you would adjust the
host_pattern and connection parameters.
@pytest.mark.ansible(host_pattern="local,", connection="local")
def test_copy_local(ansible_module):
# create a file with random data
contacted = ansible_module.copy(
dest="/etc/motd",
content="PyTest is amazing!",
owner="root",
group="root",
mode="0644",
)
# assert only a single host was contacted
assert len(contacted) == 1, "Unexpected number of hosts contacted (%d != %d)" % (
1,
len(contacted),
)
assert "local" in contacted
# assert the copy module reported changes
assert "changed" in contacted["local"]
assert contacted["local"]["changed"]Note, the parameters provided by pytest.mark.ansible will apply to all class
methods.
@pytest.mark.ansible(host_pattern="local,", connection="local")
class Test_Local(object):
def test_install(self, ansible_module):
"""do some testing"""
def test_template(self, ansible_module):
"""do some testing"""
def test_service(self, ansible_module):
"""do some testing"""When using the ansible_adhoc, localhost or ansible_module fixtures, the
object returned will be an instance of class AdHocResult. The AdHocResult
class can be inspected as follows:
def test_adhoc_result(ansible_adhoc):
contacted = ansible_adhoc(inventory=my_inventory).command("date")
# As a dictionary
for host, result in contacted.items():
assert result.is_successful, "Failed on host %s" % host
for result in contacted.values():
assert result.is_successful
for host in contacted.keys():
assert host in ["localhost", "one.example.com"]
assert contacted.localhost.is_successful
# As a list
assert len(contacted) > 0
assert "localhost" in contacted
# As an iterator
for result in contacted:
assert result.is_successful
# With __getattr__
assert contacted.localhost.is_successful
# Or __getitem__
assert contacted["localhost"].is_successfulUsing the AdHocResult object 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 the ModuleResult interface. The ModuleResult class represents
the dictionary returned by the ansible module for a particular host. The
contents of the dictionary depend on the module called.
The ModuleResult interface provides some convenient properties to determine
the success of the module call. Examples are included below.
def test_module_result(localhost):
contacted = localhost.command("find /tmp")
assert contacted.localhost.is_successful
assert contacted.localhost.is_ok
assert contacted.localhost.is_changed
assert not contacted.localhost.is_failed
contacted = localhost.shell("exit 1")
assert contacted.localhost.is_failed
assert not contacted.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 specific ansible module.
If ansible is unable to connect to any inventory, an exception will be raised.
@pytest.mark.ansible(inventory='unreachable.example.com,')
def test_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")
def test_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 in contacted.items():
assert result["ping"] == "pong"
for host, result in dark.items():
assert result["failed"] == TrueRefer to the Communication section of the documentation to find out how to get in touch with us.
You can also find more information in the Ansible communication guide.
Contributions are very welcome. Tests can be run with tox, please ensure the coverage at least stays the same before you submit a pull request.
Please see the Ansible Community Code of Conduct.
Distributed under the terms of the MIT license, "pytest-ansible" is free and open source software
If you encounter any problems, please file an issue along with a detailed description.