Lab Guide: Functional testing with pytest-ansible
Learn how to do functional testing for your Ansible content using pytest-ansible.
Learning objectives
After completing this module, you will be able to:
-
Set up pytest-ansible for functional testing of Ansible modules
-
Write Python test files that validate module behavior
-
Configure
conftest.pyfor proper module and collection path resolution -
Interpret pytest output and use debugging flags
Lab briefing
pytest-ansible is a pytest plugin that bridges the gap between Python’s pytest framework and Ansible automation. It provides 3 key capabilities:
-
Ansible execution in tests: Run Ansible modules and tasks directly from your test code
-
Collection unit test runner: Use pytest as a test runner for Ansible collections
-
Molecule integration: Access molecule scenarios through pytest fixtures
This integration allows you to write fast, isolated tests for Ansible modules, plugins, and other components using familiar Python testing patterns. While Molecule excels at integration testing with real infrastructure, pytest-ansible focuses on functional and unit-level validation of individual Ansible components.
In this lab, you’ll learn how to test the custom cowsay module you developed earlier using pytest-ansible.
Lab guide: Hands-on tasks
Why pytest-ansible?
pytest-ansible integrates Ansible with pytest by exposing fixtures that allow Ansible content to be executed and inspected as part of standard Python tests.
This is especially useful for:
-
Custom modules
-
Filter plugins
-
Lookup plugins
-
Small, fast validation tests
These tests run quickly and are well-suited for CI pipelines.
Test scope
In this task, we will test the custom cowsay module developed earlier in the collection:
-
Module:
mynamespace.mycollection.cowsay -
Test type: Functional test
-
Target: Local test host
Task 1: Create the test inventory
-
Create the
inventoryfile inside themycollection/testsdirectory:[local] localhost ansible_connection=local ansible_python_interpreter=python3
Task 2: Create the test configuration
-
Inside the
mycollection/tests/directory, create aconftest.pyfile with the following content:import os import sys import pytest # This block executes immediately when pytest starts, # ensuring Ansible sees the correct paths before loading any plugins. # Get the absolute path of the 'tests' directory TESTS_DIR = os.path.dirname(os.path.abspath(__file__)) # Project root (mycollection folder) PROJECT_ROOT = os.path.dirname(TESTS_DIR) # 1. Set ANSIBLE_LIBRARY so Ansible can find 'cowsay' by short name # This points to: .../mycollection/plugins/modules # Without this, Ansible won't know where to look for your custom module MODULES_PATH = os.path.join(PROJECT_ROOT, 'plugins', 'modules') os.environ['ANSIBLE_LIBRARY'] = MODULES_PATH # 2. Set ANSIBLE_COLLECTIONS_PATH so Ansible can resolve the full namespace # This points to: .../myansibleproject/collections # We navigate up 3 levels from PROJECT_ROOT: # mycollection -> mynamespace -> ansible_collections -> collections root # This allows Ansible to find modules using FQCN (mynamespace.mycollection.cowsay) COLLECTIONS_PATH = os.path.abspath(os.path.join(PROJECT_ROOT, '../../..')) os.environ['ANSIBLE_COLLECTIONS_PATH'] = COLLECTIONS_PATH # Optional: Uncomment to debug path configuration issues # print(f"DEBUG: ANSIBLE_LIBRARY={os.environ['ANSIBLE_LIBRARY']}") # print(f"DEBUG: ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']}")The
conftest.pyfile is a special pytest configuration file that runs before any tests. If these paths aren’t set correctly, Ansible won’t be able to locate your custom module, resulting in "module not found" errors.
Task 3: Create the test file
-
Inside the
mycollection/tests/directory, create atest_cowsay.pyfile and add the following content:def test_cowsay_module(ansible_module): """ Test the cowsay module functionality. The ansible_module fixture is provided by pytest-ansible and allows us to execute Ansible modules directly from Python test code. """ # 1. Define the input test_message = "Hello, pytest-ansible!" module_args = { "message": test_message } # 2. Run the module # The ansible_module fixture executes the module on all inventory hosts # In our case, that's just 'localhost' as defined in the inventory file result = ansible_module.cowsay(**module_args) # 3. Extract results for the localhost # Result is a dictionary keyed by hostname host_result = result['localhost'] # 4. Validation - Check module execution status # Verify the module didn't fail assert not host_result.get('failed', False), "Module execution failed" # Verify the module didn't report changes (cowsay shouldn't change system state) assert not host_result['changed'], "Module unexpectedly reported changes" # 5. Reconstruct the output string from the returned message list output_text = "\n".join(host_result['message']) # 6. Validate the output content # Check that our input message appears somewhere in the cowsay output assert test_message in output_text, f"Expected message '{test_message}' not found in output" # Verify it's actually cowsay output by checking for characteristic elements # The cow's body typically contains these border characters assert "_" in output_text or "-" in output_text, "Output doesn't look like cowsay format" assert "\\" in output_text or "/" in output_text, "Missing cowsay speech bubble borders" assert "(oo)" in output_text, "Checking for the cow eyes failed"This test validates both the module’s execution (no failures, correct change state) and its output (contains the expected message with cowsay formatting). The assertions check for structural elements that are always present in cowsay output, making the test more reliable than checking for specific ASCII art.
Task 4: Run the test
-
Open the VS Code Terminal if you don’t have one
-
Verify the VS Code Terminal is in the venv:
cd /home/rhel/myansibleproject/collections/ansible_collections/mynamespace/mycollection source .venv/bin/activate -
From the root of the collection repository (
mycollection/), run:pytest -
Expected output:
tests/integration/test_integration.py::test_integration[NOTSET] s 33% ███▍ [gw0] SKIPPED tests/integration/test_integration.py tests/unit/test_basic.py::test_basic ✓ 67% ██████▋ [gw0] PASSED tests/unit/test_basic.py tests/test_cowsay.py::test_cowsay_module ✓ 100% ██████████ [gw1] PASSED tests/test_cowsay.py [... warnings here ...] Results (1.15s): 2 passed 1 skippedYou might see warnings between the Results and PASSED tests/test_cowsay.py. That’s ok.
Task 5: Verify the test results
To verify that the test is actually running your module correctly:
-
Run pytest with verbose output to see detailed test information:
pytest -vThis shows which test functions ran and their pass/fail status.
-
Run with output capture disabled to see the actual module output:
pytest -sThis displays any print statements or debug output, useful for seeing what your module returns.
-
Combine both flags for maximum detail:
pytest -v -s -
Examine the test assertions: Review the test output to confirm:
-
The module was found and executed successfully
-
All assertions passed (module didn’t fail, didn’t report changes, output contains expected content)
-
The test validated the cowsay-specific formatting
-
Molecule vs pytest-ansible
| Tool | Purpose |
|---|---|
Molecule |
Integration and role testing using ephemeral infrastructure |
pytest-ansible |
Unit and functional testing of individual modules and plugins |
Molecule validates how content behaves in a system context, while pytest-ansible validates how individual components behave in isolation.
When to use each tool
-
Use Molecule when testing:
-
Roles
-
Complex interactions
-
OS-specific behavior
-
-
Use pytest-ansible when testing:
-
Custom modules
-
Plugins
-
Small, fast logic checks
-
Both tools complement each other and are commonly used together in professional Ansible content pipelines.
Key takeaways
-
Molecule ensures your automation works in real environments.
-
pytest-ansible ensures your building blocks behave correctly.
-
Using both provides layered confidence in your Ansible content.
Troubleshooting
If tests fail, use these debugging techniques:
| Flag | Purpose |
|---|---|
|
Verbose mode: Shows detailed test names and results, including which specific test function failed |
|
No capture: Disables output capture so you can see print statements, debug messages, and actual module output during test execution |
|
Combined: Maximum visibility into test execution and output |
Common issues:
-
Module not found: Check that
conftest.pypaths are correct and your collection structure matches expectations -
Assertion failures: Use
-sto see the actual output and verify what the module returned -
Import errors: Ensure pytest-ansible is installed and your Python environment is activated