| name | ansible-molecule-guide |
| description | Reference knowledge base for the ansible-molecule agent. Loaded by that agent on its first iteration when the host has not already injected it; not intended for direct invocation. |
Molecule Testing Guide
A comprehensive guide to writing quality Molecule tests for Ansible roles and playbooks.
This document serves as the knowledge base for the ansible-molecule agent.
Table of Contents
Overview
Molecule provides a framework for developing and testing Ansible roles and playbooks.
It enables:
- Reproducible testing - consistent test environments via containers or VMs
- Multi-platform validation - test across different OS families
- Idempotence verification - ensure roles can run multiple times safely
- Assertion-based verification - programmatic validation of role outcomes
Core Principles
- Every role needs tests - untested code is unreliable code
- Tests must assert outcomes - checking existence is not enough
- Idempotence is mandatory - roles must be safe to run repeatedly
- Multi-platform coverage - test all supported platforms
- Fast feedback - tests should run quickly in CI
Testing Philosophy
Molecule implements the Four-Phase Test Pattern - a structured approach that makes
test objectives clear:
- Setup (create/prepare) - Provision clean, isolated test environments
- Exercise (converge) - Execute the Ansible content being tested
- Verify (verify) - Validate the desired outcomes were achieved
- Teardown (destroy) - Remove all created artifacts
BDD Workflow (Given-When-Then)
The recommended development workflow maps to BDD:
| BDD Phase | Molecule Command | Purpose |
|---|
| Given | molecule create | Environment is provisioned |
| When | molecule converge | Role/playbook is applied |
| Then | molecule verify | Outcomes are validated |
Test Isolation and Reproducibility
Each test must run in predictable, isolated conditions that:
- Don't interfere with other tests or external systems
- Produce consistent results across different environments
- Work identically for all team members and CI/CD pipelines
Resource Lifecycle
Molecule manages the complete lifecycle:
- Environment provisioning - Create clean, isolated test environments
- Dependency resolution - Ensure all required resources are available
- Change application - Execute the system logic being tested
- Idempotence verification - Confirm operations produce no unintended changes
- Functional verification - Validate desired outcomes were achieved
- Side effect detection - Identify unintended consequences
- Resource cleanup - Remove all created artifacts
Scenario Structure
A Molecule scenario defines a complete test environment and sequence.
Directory Layout
roles/my_role/
└── molecule/
├── default/ # Default scenario (required)
│ ├── molecule.yml # Scenario configuration
│ ├── converge.yml # Playbook to apply the role
│ ├── verify.yml # Verification playbook
│ ├── prepare.yml # Pre-test setup (optional)
│ ├── cleanup.yml # Post-test cleanup (optional)
│ ├── side_effect.yml # Side effect playbook (optional)
│ └── requirements.yml # Role/collection dependencies
└── security/ # Additional scenario
├── molecule.yml
├── converge.yml
└── verify.yml
File Purposes
| File | Purpose |
|---|
molecule.yml | Scenario configuration: platforms, provisioner, test sequence |
converge.yml | Applies the role under test with test variables |
verify.yml | Validates the role produced the expected outcomes |
prepare.yml | Sets up prerequisites before converge (optional) |
cleanup.yml | Tears down resources after tests (optional) |
requirements.yml | Dependencies for the scenario |
Multiple Scenarios
Use multiple scenarios for different test purposes:
molecule/
├── default/ # Standard functionality
├── security/ # Security-focused tests
├── upgrade/ # Upgrade path testing
└── minimal/ # Minimal configuration
molecule.yml Configuration
The molecule.yml file is the central configuration entrypoint for Molecule.
Configuration Hierarchy
Settings are inherited from multiple levels (most specific wins):
$HOME/.config/molecule/config.yml - Global defaults for all projects
- Project root
config.yml - Project-level defaults
extensions/molecule/config.yml - Collection-specific defaults
molecule/*/molecule.yml - Scenario-specific overrides
Empty molecule.yml files inherit the complete configuration from parent configs.
Environment Variable Substitution
Molecule supports shell-style variable substitution in molecule.yml:
platforms:
- name: ${MOLECULE_INSTANCE_NAME:-default}
image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest"
provisioner:
env:
MY_VAR: ${MY_VAR}
WITH_DEFAULT: ${VAR:-default}
LITERAL: $$NOT_A_VAR
Important: Avoid the MOLECULE_ prefix for custom variables - it's reserved.
Complete Example
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: docker
platforms:
- name: ubuntu-22
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: ""
groups:
- debian
- name: rocky-9
image: geerlingguy/docker-rockylinux9-ansible:latest
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: ""
groups:
- redhat
provisioner:
name: ansible
log: true
config_options:
defaults:
callbacks_enabled: profile_tasks
fact_caching: jsonfile
fact_caching_connection: /tmp/facts_cache
ssh_connection:
pipelining: true
inventory:
group_vars:
all:
ansible_user: root
playbooks:
converge: converge.yml
verify: verify.yml
prepare: prepare.yml
verifier:
name: ansible
scenario:
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- verify
- cleanup
- destroy
Key Sections
dependency
Installs role and collection dependencies:
dependency:
name: galaxy
options:
requirements-file: requirements.yml
force: true
driver
Specifies the infrastructure driver. Molecule comes with three drivers pre-installed:
driver:
name: docker
driver:
name: podman
driver:
name: delegated
Podman vs Docker:
- Podman: Lightweight, rootless mode for increased security, no running daemon
- Docker: More widely supported, better tooling ecosystem
Flexible Backend with molecule-containers:
For scenarios requiring backend flexibility, install molecule-containers:
pip install molecule-containers
Configure via environment variable:
export MOLECULE_CONTAINERS_BACKEND=podman,docker
platforms
Defines test instances:
platforms:
- name: instance-name
image: docker/image:tag
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: ""
groups:
- webservers
docker_networks:
- name: molecule_net
provisioner
Configures Ansible execution:
provisioner:
name: ansible
log: true
config_options:
defaults:
callbacks_enabled: profile_tasks
verbosity: 1
env:
ANSIBLE_DIFF_ALWAYS: "true"
inventory:
host_vars:
instance-name:
custom_var: value
group_vars:
all:
common_var: value
scenario
Controls test execution sequence and scenario behavior:
scenario:
name: default
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- verify
- cleanup
- destroy
CRITICAL: The idempotence step is mandatory unless explicitly documented.
Task Filtering Tags:
Skip tasks during specific Molecule phases using tags:
| Tag | Effect |
|---|
molecule-notest | Skip task in ALL Molecule phases |
notest | Alias for molecule-notest |
molecule-idempotence-notest | Skip ONLY during idempotence check |
- name: Seed database (not idempotent)
ansible.builtin.command: /usr/bin/seed-db
tags:
- molecule-idempotence-notest
- name: Task not suitable for containers
ansible.builtin.reboot:
tags:
- molecule-notest
Shared State Between Scenarios:
Enable resource sharing between scenarios to reduce execution time:
shared_state: true
When enabled, scenarios can access infrastructure created by the default scenario,
eliminating per-scenario setup/teardown overhead.
Custom Sequences:
Define separate sequences for different operations:
scenario:
create_sequence:
- dependency
- create
- prepare
converge_sequence:
- converge
destroy_sequence:
- destroy
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- verify
- cleanup
- destroy
Additional Settings
Prerun Configuration:
Molecule automatically installs dependencies via prerun. Disable if needed:
prerun: false
Role Name Check:
By default, Molecule validates role names follow namespace.role standard.
Relax validation for non-conforming roles:
role_name_check: 1
Note: Following the namespace and role naming standard is strongly recommended.
Converge Playbooks
The converge playbook applies the role under test.
Standard Structure
---
- name: Converge
hosts: all
become: true
gather_facts: true
vars:
nginx_port: 8080
nginx_worker_processes: 2
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_facts['os_family'] == 'Debian'
roles:
- role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}"
Best Practices
-
Use FQCN for all modules - even in test playbooks
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
- name: Update apt cache
apt:
update_cache: true
-
Set become at play level if role requires it
-
Use environment variable for role name
roles:
- role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}"
-
Keep converge minimal - only what's needed to apply the role
-
Use vars for test-specific overrides - don't modify role defaults
Role Inclusion Methods
roles:
- role: my_role
vars:
my_role_var: value
tasks:
- name: Include role under test
ansible.builtin.include_role:
name: my_role
vars:
my_role_var: value
roles:
- role: my_namespace.my_collection.my_role
Verify Playbooks
The verify playbook validates that the role produced expected outcomes.
Verifier Options
Molecule supports multiple verifiers:
| Verifier | Language | Best For |
|---|
| ansible (default) | YAML | Simple tests (2-5 tasks), no extra language required |
| testinfra | Python | Complex test suites, parametrized tests, richer assertions |
| goss | YAML | Fast validation, simple syntax |
Ansible became the default verifier in Molecule v3 to provide a unified testing
experience without requiring Python knowledge for Testinfra.
When to use Testinfra:
- More than 5 verification tasks
- Need loops or complex conditional logic
- Evolving test suite with frequent additions
- Need parametrized tests across multiple values
When to stick with Ansible:
- Simple tests that fit in 2-5 tasks
- Team prefers YAML over Python
- Quick validation of basic outcomes
Critical Requirements
Every verify.yml MUST have assertions. A verify playbook without assertions
is useless - it provides no validation.
Standard Structure
---
- name: Verify
hosts: all
become: true
gather_facts: true
vars_files:
- ../../defaults/main.yml
- ../../vars/main.yml
tasks:
- name: Check nginx is installed
ansible.builtin.package:
name: nginx
state: present
check_mode: true
register: nginx_installed
failed_when: nginx_installed.changed
- name: Gather service facts
ansible.builtin.service_facts:
- name: Assert nginx service is running
ansible.builtin.assert:
that:
- "'nginx.service' in ansible_facts.services"
- "ansible_facts.services['nginx.service'].state == 'running'"
fail_msg: "nginx service is not running"
success_msg: "nginx service is running as expected"
- name: Read nginx config
ansible.builtin.slurp:
src: /etc/nginx/nginx.conf
register: nginx_config
- name: Assert nginx config contains expected content
ansible.builtin.assert:
that:
- "'worker_processes' in nginx_config.content | b64decode"
fail_msg: "nginx.conf missing worker_processes directive"
- name: Check nginx is listening on port 80
ansible.builtin.wait_for:
port: 80
timeout: 10
register: port_check
failed_when: port_check.failed
- name: Verify nginx responds
ansible.builtin.uri:
url: "http://localhost:80"
status_code: 200
register: http_response
retries: 3
delay: 5
until: http_response.status == 200
Assertion Patterns
Check Package Installation
- name: Check package is installed
ansible.builtin.package:
name: "{{ package_name }}"
state: present
check_mode: true
register: pkg_check
failed_when: pkg_check.changed
Check Service Status
- name: Gather service facts
ansible.builtin.service_facts:
- name: Assert service is running and enabled
ansible.builtin.assert:
that:
- "ansible_facts.services['{{ service_name }}.service'].state == 'running'"
- "ansible_facts.services['{{ service_name }}.service'].status == 'enabled'"
fail_msg: "{{ service_name }} is not running or not enabled"
Check File Existence and Permissions
- name: Stat configuration file
ansible.builtin.stat:
path: /etc/app/config.yml
register: config_stat
- name: Assert config file exists with correct permissions
ansible.builtin.assert:
that:
- config_stat.stat.exists
- config_stat.stat.mode == '0644'
- config_stat.stat.pw_name == 'app'
fail_msg: "Config file missing or has wrong permissions"
Check File Content
- name: Read config file
ansible.builtin.slurp:
src: /etc/app/config.yml
register: config_content
- name: Assert config contains expected values
ansible.builtin.assert:
that:
- "'port: 8080' in config_content.content | b64decode"
- "'debug: false' in config_content.content | b64decode"
fail_msg: "Config file missing expected content"
Check Port Listening
- name: Check application is listening
ansible.builtin.wait_for:
port: 8080
host: localhost
timeout: 30
register: port_wait
- name: Assert port is open
ansible.builtin.assert:
that: not port_wait.failed
fail_msg: "Application is not listening on port 8080"
Check HTTP Endpoint
- name: Query health endpoint
ansible.builtin.uri:
url: http://localhost:8080/health
return_content: true
register: health_check
- name: Assert health check passes
ansible.builtin.assert:
that:
- health_check.status == 200
- "'healthy' in health_check.content"
fail_msg: "Health check failed: {{ health_check.content }}"
Weak vs Strong Assertions
- name: Check config exists
ansible.builtin.stat:
path: /etc/app/config
register: config_stat
- name: Check config exists
ansible.builtin.stat:
path: /etc/app/config
register: config_stat
- name: Assert config exists
ansible.builtin.assert:
that: config_stat.stat.exists
fail_msg: "Config file does not exist"
- name: Read config
ansible.builtin.slurp:
src: /etc/app/config
register: config_content
- name: Assert config has required settings
ansible.builtin.assert:
that:
- "'database_host' in config_content.content | b64decode"
- "'database_port' in config_content.content | b64decode"
fail_msg: "Config missing required database settings"
Multi-Platform Testing
Testing across multiple platforms catches platform-specific bugs.
Platform Selection
Test all platforms your role supports:
platforms:
- name: ubuntu-20
image: geerlingguy/docker-ubuntu2004-ansible:latest
pre_build_image: true
groups: [debian]
- name: ubuntu-22
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
groups: [debian]
- name: debian-11
image: geerlingguy/docker-debian11-ansible:latest
pre_build_image: true
groups: [debian]
- name: rocky-8
image: geerlingguy/docker-rockylinux8-ansible:latest
pre_build_image: true
groups: [redhat]
- name: rocky-9
image: geerlingguy/docker-rockylinux9-ansible:latest
pre_build_image: true
groups: [redhat]
- name: fedora-39
image: geerlingguy/docker-fedora39-ansible:latest
pre_build_image: true
groups: [redhat]
Platform Groups
Use groups for platform-specific tests:
- name: Verify on Debian
hosts: debian
tasks:
- name: Check apt package
ansible.builtin.package:
name: nginx
state: present
check_mode: true
- name: Verify on RedHat
hosts: redhat
tasks:
- name: Check yum package
ansible.builtin.package:
name: nginx
state: present
check_mode: true
Systemd Container Requirements
For containers running systemd:
platforms:
- name: ubuntu-systemd
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: ""
Single Platform Anti-Pattern
Problem: Only testing on one platform when role supports multiple
platforms:
- name: ubuntu
image: geerlingguy/docker-ubuntu2204-ansible:latest
Solution: Test all supported platforms
platforms:
- name: ubuntu-22
image: geerlingguy/docker-ubuntu2204-ansible:latest
groups: [debian]
- name: rocky-9
image: geerlingguy/docker-rockylinux9-ansible:latest
groups: [redhat]
Idempotence Testing
Idempotence ensures a role can run multiple times safely.
Why Idempotence Matters
- Repeated runs must not break things - Production runs Ansible regularly
- Changes should only happen once - Second run should report no changes
- Catches state bugs - Reveals tasks that always report "changed"
Enabling Idempotence
Include idempotence in test_sequence:
scenario:
test_sequence:
- converge
- idempotence
- verify
How It Works
- Molecule runs converge
- Molecule runs converge again
- If any task reports "changed", the idempotence step fails
Handling Non-Idempotent Tasks
Some tasks legitimately change every run. Handle with changed_when:
- name: Get current timestamp
ansible.builtin.command: date
register: current_date
changed_when: false
- name: Run migration
ansible.builtin.command: ./migrate.sh
register: migration
changed_when: "'Applied' in migration.stdout"
Common Idempotence Failures
-
command/shell without changed_when
- name: Get status
ansible.builtin.command: systemctl status nginx
- name: Get status
ansible.builtin.command: systemctl status nginx
register: status
changed_when: false
-
Template with dynamic content
-
Missing creates/removes
- name: Initialize database
ansible.builtin.command: ./init_db.sh
- name: Initialize database
ansible.builtin.command: ./init_db.sh
args:
creates: /var/lib/db/.initialized
Prepare and Cleanup
Prepare Playbook
Runs before converge to set up prerequisites:
---
- name: Prepare
hosts: all
become: true
gather_facts: true
tasks:
- name: Install required dependencies
ansible.builtin.package:
name:
- python3
- python3-pip
state: present
- name: Create required directories
ansible.builtin.file:
path: /opt/app
state: directory
mode: "0755"
- name: Pre-populate configuration
ansible.builtin.template:
src: test-config.yml.j2
dest: /etc/app/test-config.yml
Cleanup Playbook
Runs after verify to clean up resources:
---
- name: Cleanup
hosts: all
become: true
gather_facts: false
tasks:
- name: Remove test data
ansible.builtin.file:
path: /tmp/test-data
state: absent
- name: Stop test services
ansible.builtin.service:
name: test-service
state: stopped
ignore_errors: true
When to Use Prepare/Cleanup
Use prepare when:
- Role requires pre-existing resources
- Testing upgrade scenarios
- Setting up test data
Use cleanup when:
- Tests create persistent resources
- Need to reset state between scenarios
- Running in shared environments
Side Effects and Advanced Patterns
Side Effect Playbook
The side_effect.yml playbook executes actions that produce side effects on instances.
It's designed for testing HA failover scenarios, service restarts, or configuration changes.
---
- name: Side Effect - Simulate failover
hosts: all
become: true
tasks:
- name: Stop primary service
ansible.builtin.service:
name: myapp
state: stopped
- name: Wait for failover
ansible.builtin.pause:
seconds: 10
Advanced Multi-Step Testing
Molecule supports complex stateful testing with multiple side effects and verifications.
Actions can take optional arguments to specify different playbooks/tests:
scenario:
test_sequence:
- converge
- side_effect reboot.yaml
- verify after_reboot/
- side_effect alter_configs.yaml
- converge
- verify test2.py test3.py
- side_effect
- verify
This pattern enables testing:
- System reboots: Verify state persists across restarts
- Configuration drift: Ensure role corrects manual changes
- Upgrade scenarios: Test transitions between versions
- Failure recovery: Validate HA and failover behavior
Multiple Converge Steps
You can run converge multiple times with different configurations:
scenario:
test_sequence:
- converge
- converge upgrade.yml
- idempotence
- verify
Development Workflow
Iterative Development (Recommended)
For efficient role development, create instances once and iterate:
molecule create
molecule converge
molecule converge
molecule verify
molecule test
molecule destroy
This approach eliminates environment setup friction and provides rapid feedback.
Test-Driven Development (TDD)
Write tests before implementation:
- Write verify.yml first - Define expected outcomes
- Run
molecule converge - See tests fail (red)
- Implement role tasks - Make tests pass (green)
- Run
molecule test - Validate everything including idempotence
- Refactor - Improve code knowing tests catch regressions
Quick Reference Commands
| Command | Purpose |
|---|
molecule create | Create instances only |
molecule converge | Apply role (keep instances) |
molecule idempotence | Check idempotence only |
molecule verify | Run verification only |
molecule test | Full test cycle |
molecule destroy | Remove instances |
molecule login | SSH into instance |
molecule login -h instance-name | SSH into specific instance |
molecule test --all | Run all scenarios |
molecule test -s scenario-name | Run specific scenario |
molecule list | Show instance status |
Debugging Failed Tests
molecule test --destroy=never
molecule login
molecule --debug test
molecule converge -- -vvv
CI/CD Integration
Best Practices for CI/CD
-
Use fail-fast: false - When testing across multiple distributions, an error
on one might not occur on another. Let all matrix jobs complete.
-
Matrix test across distributions - Use environment variables to test multiple
OS versions in parallel.
-
Cache dependencies - Speed up builds by caching pip packages.
-
Use colored output - Set PY_COLORS=1 and ANSIBLE_FORCE_COLOR=1 for
readable logs.
-
Least privilege - Grant only necessary permissions to workflow tokens.
GitHub Actions Example
name: Molecule Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
distro:
- ubuntu2204
- ubuntu2404
- debian12
- rockylinux9
scenario:
- default
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
pip install molecule molecule-plugins[docker] ansible-lint yamllint
- name: Run Molecule
run: molecule test -s ${{ matrix.scenario }}
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
MOLECULE_DISTRO: ${{ matrix.distro }}
Using MOLECULE_DISTRO in molecule.yml:
platforms:
- name: instance
image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest"
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
Multi-Scenario Testing
jobs:
molecule:
strategy:
fail-fast: false
matrix:
scenario:
- default
- security
- upgrade
steps:
- name: Run Molecule
run: molecule test -s ${{ matrix.scenario }}
GitLab CI Example
molecule:
image: python:3.12
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
before_script:
- pip install molecule molecule-plugins[docker] ansible-lint
script:
- molecule test -s $MOLECULE_SCENARIO
parallel:
matrix:
- MOLECULE_SCENARIO: [default, security]
MOLECULE_DISTRO: [ubuntu2204, rockylinux9]
Parallel Execution
Molecule supports parallel execution of scenarios:
molecule test --parallel --all
Note: Ensure scenarios don't conflict when running in parallel (unique container
names, ports, etc.).
Common Anti-Patterns
1. Missing Verify Playbook
Problem: No verify.yml means no validation
molecule/default/
├── molecule.yml
└── converge.yml
Solution: Always include verify.yml with assertions
2. Empty Assertions
Problem: verify.yml exists but has no assertions
- name: Verify
hosts: all
tasks:
- name: Check file
ansible.builtin.stat:
path: /etc/app/config
register: config_stat
Solution: Add meaningful assertions
- name: Assert config exists
ansible.builtin.assert:
that: config_stat.stat.exists
fail_msg: "Config file does not exist"
3. Missing Idempotence
Problem: No idempotence step in test_sequence
scenario:
test_sequence:
- converge
- verify
Solution: Always include idempotence
scenario:
test_sequence:
- converge
- idempotence
- verify
4. Single Platform on Multi-Platform Role
Problem: Role supports multiple OS but tests only one
platforms:
- name: ubuntu
image: geerlingguy/docker-ubuntu2204-ansible:latest
Solution: Test all supported platforms
5. Non-FQCN in Test Playbooks
Problem: Using short module names
- name: Check package
stat:
path: /usr/bin/nginx
Solution: Use FQCN
- name: Check package
ansible.builtin.stat:
path: /usr/bin/nginx
6. Missing gather_facts in Verify
Problem: Using ansible_facts without gathering them
- name: Verify
hosts: all
gather_facts: false
tasks:
- name: Check service
ansible.builtin.assert:
that: ansible_facts.services['nginx'].state == 'running'
Solution: Enable gather_facts
- name: Verify
hosts: all
gather_facts: true
tasks:
- name: Gather service facts
ansible.builtin.service_facts:
- name: Check service
ansible.builtin.assert:
that: ansible_facts.services['nginx.service'].state == 'running'
7. Hardcoded Values in Tests
Problem: Tests use hardcoded values that don't match role defaults
- name: Check port
ansible.builtin.wait_for:
port: 80
Solution: Use vars_files or variables
- name: Verify
hosts: all
vars_files:
- ../../defaults/main.yml
tasks:
- name: Check port
ansible.builtin.wait_for:
port: "{{ nginx_port }}"
8. Testing Implementation Details
Problem: Tests verify internal implementation rather than observable outcomes
- name: Check temp file exists
ansible.builtin.stat:
path: /tmp/role-internal-marker
register: marker
- name: Assert marker exists
ansible.builtin.assert:
that: marker.stat.exists
Solution: Test observable behavior and outcomes
- name: Verify service responds correctly
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
9. Excessive Setup in Converge
Problem: Converge playbook does too much beyond applying the role
- name: Converge
hosts: all
tasks:
- name: Install prerequisites
ansible.builtin.package:
name: [python3, curl, wget]
state: present
- name: Create directories
ansible.builtin.file:
path: /opt/app
state: directory
- name: Include role
ansible.builtin.include_role:
name: my_role
Solution: Move setup to prepare.yml
- name: Converge
hosts: all
roles:
- role: my_role
10. Testing-Only Dependencies Not Isolated
Problem: Role dependencies installed unconditionally
Solution: Use environment variable to conditionally include test dependencies
dependencies:
- role: test_helper_role
when: lookup('env', 'MOLECULE_FILE') | length > 0
Quality Checklist
molecule.yml
converge.yml
verify.yml
prepare.yml (if used)
Idempotence
General
Troubleshooting
Common Issues
Idempotence fails but role works correctly:
- Run
molecule converge twice manually
- Check which tasks report
changed
- Add
changed_when: false or use creates/removes
Service facts empty:
- name: Gather service facts
ansible.builtin.service_facts:
- name: Check service
ansible.builtin.assert:
that: ansible_facts.services['nginx.service'].state == 'running'
Container fails to start with systemd:
platforms:
- name: instance
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
command: ""
Role library/modules not available in verify.yml:
- name: Verify
hosts: all
tasks:
- name: Include role for library access
ansible.builtin.include_role:
name: my_role
tasks_from: init.yml
References
Official Documentation
Community Resources
Related Tools
Last updated: 2026-02-06