Since diving back into Ansible as of late, I got to a point where the constant manual testing of the roles got a bit stale. The best way for me is to setup test would be inside a pipeline that runs every push to see if everything works. Molecule gives the ability to test your Ansible playbooks/roles locally or inside a CI job (Github, Jenkins, Gitea,…).

Installing molecule

Easily done using pip:

pip install molecule

You can always check if it’s working and is installed:

molecule --version

molecule 25.4.0 using python 3.13
    ansible:2.18.5
    default:25.4.0 from molecule

Setting up Molecule

There are multiple ways to setup molecule to test your playbooks/roles.

Create new role using Molecule

If you are starting from scratch and want the whole directory structure to be setup for you, you can init a role like this:

molecule init role imrein.testrole -d docker

This will create a role with an extra directory for molecule. This can be modified to fit your needs:

ls -l imrein.testrole/molecule/default/

Dockerfile.j2
INSTALL.rst
molecule.yml
playbook.yml
tests

Add molecule to existing role

Adding Molecule to an existing role is also easy. There are a few files you will need.

Create the directories:

cd imrein.testrole2/
mkdir -p molecule/default/

Create files:

cd imrein.testrole2/molecule/default/
touch converge.yml molecule.yml

molecule.yml

---
role_name_check: 1
dependency:
  name: galaxy
  options:
    ignore-errors: true
driver:
  name: docker
platforms:
  - name: instance
    image: "geerlingguy/docker-${MOLECULE_DISTRO:-rockylinux9}-ansible:latest"
    command: ${MOLECULE_DOCKER_COMMAND:-""}
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:rw
    cgroupns_mode: host
    privileged: true
    pre_build_image: true
provisioner:
  name: ansible
  playbooks:
    converge: ${MOLECULE_PLAYBOOK:-converge.yml}

This is the configuration file for molecule. This defines how molecule will run using the correct platform and test playbook(s). For the images I used some premade ones created by Jeff Geerling which have Ansible and systemd on them.

I also use volumes to mount /sys/fs/cgroup inside the image, this way systemd won’t have any problems running.

converge.yml

---
- name: Converge
  hosts: all

  pre_tasks:
    - name: Update apt cache
      apt: update_cache=yes cache_valid_time=600
      when: ansible_os_family == 'Debian'

    - name: Wait for systemd to complete initialization
      command: systemctl is-system-running
      register: systemctl_status
      until: >
        'running' in systemctl_status.stdout or
        'degraded' in systemctl_status.stdout        
      retries: 30
      delay: 5
      when: ansible_service_mgr == 'systemd'
      changed_when: false
      failed_when: systemctl_status.rc > 1

  roles:
    - role: imrein.monitoring

Molecule will follow a default test sequence. For every step you can create a different yaml file and write some tasks to perform testing on the role. Here I utilized the converge step which will invoke the playbook with ansible-playbook and run this against an instance created by the driver.

In the example above it will just test and see if systemd is running. We can take it a step further and even check if certain newly installed components are running:

verify.yml

---
- name: Verify Docker Role
  hosts: all
  tasks:
    # Prometheus
    - name: Verify Prometheus binary is installed
      command: prometheus --version
      register: prometheus_version
      changed_when: false
      failed_when: prometheus_version.rc != 0

    - name: Show Prometheus version details
      debug:
        msg: >
          Prometheus Version Output:
          {{ prometheus_version.stdout_lines | join('\n') }}          

    - name: Verify Prometheus service is running
      command: systemctl is-active prometheus
      register: prometheus_version
      when: ansible_service_mgr == 'systemd'
      changed_when: false
      failed_when: prometheus_version.stdout.strip() != "active"

Utilizing CI

As I’m running Gitea at home, I can utilize the actions to automate this testing.

*See my ansible-monitoring role for an example.

Gitea actions

I will asume the basics of Gitea/Github actions is known and go straight to the jobs file:

---
name: CI
run-name: Molecule testing
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

defaults:
  run:
    working-directory: imrein.monitoring

jobs:
  Linting:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the codebase.
        uses: actions/checkout@v4
        with:
          path: 'imrein.monitoring'

      - name: Set up python3
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Install test dependencies.
        run: pip3 install yamllint

      - name: Lint code.
        run: |
          yamllint .          

  Molecule:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        distro:
          - rockylinux9
          - ubuntu2404
          - ubuntu2204
          - debian12
          - fedora41
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          path: 'imrein.monitoring'

      - name: Setup python3
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Install test dependencies
        run: pip3 install ansible molecule molecule-plugins[docker] docker

      - name: Run molecule test
        run: molecule test
        env:
          PY_COLORS: '1'
          ANSIBLE_FORCE_COLOR: '1'

In here I let this job run on every push and pull request. It starts by linting everything and see if there are no yaml issues before starting the role check. After that tests the role on any distro you define.

The repo is pulled, the dependencies get installed and molecule starts the testing. diagram