Testing Ansible Automation with Molecule Pt. 1

There is a lot of Ansible out there. Let’s look at how to test roles using Molecule, Docker, and Testinfra.

Phil Critchfield
Contino Engineering
6 min readJul 1, 2022

--

What is Molecule?

Molecule is a RedHat project designed to aid in testing Ansible roles. It provides support for testing with various operating systems and distributions. Molecule is also very versatile, allowing for an array of virtualization providers, test frameworks, and test scenarios. This approach encourages consistency in developing and maintaining roles.

Getting Started

Setting up your environment

Python3 and pip are requirements for Ansible and Molecule. The installation and setup of those tools are outside the scope of this guide.

The following commands will install Ansible and Molecule using pip. For other installation options, visit their respective documentation pages, Ansible and Molecule.

You will also need docker-ce installed along with the Docker plugin for Molecule. The last command in the above script installs the docker plugin. This method is how you install any molecule plugins you plan to use. Once done, we can Docker can now serve as the driver in Molecule.

Initializing a new role with Molecule

Now that the prerequisites are ready, let’s start with some basics. Begin by running molecule init role sample.demo_role --driver-name docker --verifier-name testinfra . Boom, Molecule creates a new role already configured with the test directory. Let’s look at the directory, and then we’ll dive into that command in more detail.

How awesome is that? It created everything we need to build a role and test it simultaneously.

Examining the command and what it creates

Here is a description of the command and options we used:

molecule init role sample.demo_role
init role leverages ansible-galaxy to create a templated directory for <NAME_SPACE>.<ROLE_NAME>. The command also preps the Molecule directory to help you get started. Alternatively, if you already have a role, you can initialize Molecule as a scenario with molecule init scenario -r <ROLE_NAME>

--driver-name Docker
This tells Molecule what driver to use for your test environment. The driver specifies what framework or infrastructure to deploy for the tests. Several available drivers include podman, libvirt, Azure, and many others. Regardless of your driver, you will need to install the module using pip. For this post, we are going to focus on using Docker.

--verifier-name testinfra
This specifies what verifier you want to use for your default scenario. The verifier is the mechanism used for testing your roles. Some available options include Ansible, testinfra, and inspec. While Ansible is the default verifier, we will use testinfra for this demo.

Here is a description of the created Molecule files:

./molecule/default/molecule.yml
This is the core file for Molecule. Used to define your testing steps, scenarios, dependencies, and other configuration options. The generated file should look like this:

./molecule/default/converge.yml
This is the playbook that Molecule will run to provision the targets for testing. Below is the file that Molecule creates. If you can use this file in a playbook, you can use it here.

./molecule/default/tests/test_default.py
This is the initial test file generated for testinfra. This is where we will be putting our tests for this demo. However, you are not restricted to this file or even this directory for your tests. The default test is shown below.

Getting Practical

A role has to do something

We have a role that currently doesn’t do anything, let’s fix that by adding some tasks to ./tasks/main.yml. For this post, we’ll stick to the basics, only adding a few things we can write tests for. In this case, some yum packages, a user, and a config file. Provided in the code below.

Create and Converge

We have a role that will process some tasks, so now what? First, we can run the command molecule create in the ./demo_role directory. Create uses the settings defined in the molecule.yml config to determine the driver and the platform. For this example, Molecule will use the following lines in the molecule.yml.

It will use Ansible to launch a Docker container based on the quay.io/centos/centos:stream8 image, pulling it from the container registry if needed.

Now that we have a target and a role, let’s bring them together with molecule converge . The converge command runs the converge.yml playbook against the target platform.

INFO     default scenario test matrix: dependency, create, prepare, converge
INFO Performing prerun with role_name_check=0...
INFO Set ANSIBLE_LIBRARY=/home/pcritchfield/.cache/ansible-compat/54dfe2/modules:/home/pcritchfield/.ansible/plugins/modules:/usr/share/ansible/plugins/modules
INFO Set ANSIBLE_COLLECTIONS_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/collections:/home/pcritchfield/.ansible/collections:/usr/share/ansible/collections
INFO Set ANSIBLE_ROLES_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/roles:/home/pcritchfield/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
INFO Using /home/pcritchfield/.cache/ansible-compat/54dfe2/roles/sample.demo_role symlink to current repository in order to enable Ansible to find the role using its expected full name.
INFO Running default > dependency
WARNING Skipping, missing the requirements file.
WARNING Skipping, missing the requirements file.
INFO Running default > create
WARNING Skipping, instances already created.
INFO Running default > prepare
WARNING Skipping, prepare playbook not configured.
INFO Running default > converge
INFO Sanity checks: 'docker'
PLAY [Converge] ****************************************************************TASK [Gathering Facts] *********************************************************
ok: [instance]
TASK [Include sample.demo_role] ************************************************TASK [sample.demo_role : install some packages] ********************************
changed: [instance] => (item=epel-release)
changed: [instance] => (item=htop)
changed: [instance] => (item=nginx)
changed: [instance] => (item=git)
TASK [sample.demo_role : add webapp user] **************************************
changed: [instance]
TASK [sample.demo_role : create an app directory owned by webapp] **************
changed: [instance]
TASK [sample.demo_role : create an app directory owned by webapp] **************
changed: [instance]
PLAY RECAP *********************************************************************
instance : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Looking at that output, we can see the Molecule steps being run. On the line showing INFO default scenario test matrix: dependency, create, prepare, converge . This means that running molecule create isn’t always required. Often you can just run the converge command since it already includes create.

Write tests and Verify

Now that we have run converge and deployed the containers, let’s run some tests. Use the command molecule verify to run the default test in test_default.py. You should see output similar to:

INFO     default scenario test matrix: verify
INFO Performing prerun with role_name_check=0...
INFO Set ANSIBLE_LIBRARY=/home/pcritchfield/.cache/ansible-compat/54dfe2/modules:/home/pcritchfield/.ansible/plugins/modules:/usr/share/ansible/plugins/modules
INFO Set ANSIBLE_COLLECTIONS_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/collections:/home/pcritchfield/.ansible/collections:/usr/share/ansible/collections
INFO Set ANSIBLE_ROLES_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/roles:/home/pcritchfield/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
INFO Using /home/pcritchfield/.cache/ansible-compat/54dfe2/roles/sample.demo_role symlink to current repository in order to enable Ansible to find the role using its expected full name.
INFO Running default > verify
INFO Executing Testinfra tests found in /home/pcritchfield/Projects/ansible_molecule/roles/demo_role/molecule/default/tests/...
============================= test session starts ==============================
platform linux -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /home/pcritchfield
plugins: testinfra-6.8.0
collected 1 item
molecule/default/tests/test_default.py . [100%]============================== 1 passed in 1.50s ===============================
INFO Verifier completed successfully.

Success! Our default test works and confirms that root is a user. molecule verify also allows us to iterate over our tests on the existing container. No need to rerun create or converge.

Now let’s write some tests to confirm our role is doing what it should. We’ll start by testing that the proper packages and versions are installed. Add the following to ./molecule/default/tests/test_default.py:

For those new to testing, this may look like a lot. Fortunately, it is pretty straightforward. We are using pytest to parameterize a map of name and version. Then passing that into test_packages and checking for the package name and its version. Using the map allows us to write one test for all packages. Go ahead and save that file and run molecule verify. You should see that four tests have run successfully.

======================= test session starts ========================
platform linux -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /home/pcritchfield
plugins: testinfra-6.8.0
collected 4 items
molecule/default/tests/test_default.py .... [100%]======================== 4 passed in 2.77s =========================
INFO Verifier completed successfully.

Next, we can test for the user webapp and the file app.conf. Add the following to test_default.py:

Run molecule verify one more time, and you will now see six passing tests:

======================= test session starts ========================
platform linux -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /home/pcritchfield
plugins: testinfra-6.8.0
collected 6 items
molecule/default/tests/test_default.py ...... [100%]======================== 6 passed in 4.09s =========================

Finally, to clean up, we can run molecule destroy. This removes the containers that we deployed and provisioned with create or converge. Putting us into a great place to start again. Providing confidence that we will get consistent deployments with every use of our demo_role.

One command to rule them all

One last Molecule command to look at is molecule test. The test command will run the entire scenario; creating, converging, verifying, and more. Let’s look at what test does as the default scenario. Run the command molecule matrix test to list all of the steps that test will go through:

INFO     Test matrix
---
default:
- dependency
- lint
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- side_effect
- verify
- cleanup
- destroy

Notice that there is much more happening than just running the tests, including linting, provisioning the environment, and testing for idempotence. Molecule will run the tests in the verify step and clean up the test environment. Give it a try and run molecule test.

You can find the code demoed in this post over on GitHub

Next Time

In the next post, I’ll dive into the molecule.yml config more deeply. We’ll look at testing multiple operating systems, linting, and idempotence.

--

--

Phil Critchfield
Contino Engineering

DevOps consultant, Linux hobbyist, and Board Game enthusiast