Last Updated: July 13, 2018
·
17.52K
· tumf

How to develop Ansible roles w/ unit test and CI

How to develop Ansible roles w/ unit test and continuous integration services.

This article is a translation from my Qiita post on Dec 2015 titled AnsibleロールのユニットテストからTravis CIまで in Japanese.

Create sample role

sample codes are here:
https://github.com/tumf/ansible-unit-test-sample

For example, create sample role template by using ansible-galaxy command.

$ ansible-galaxy init sample                      

It generates as follows:

├── README.md
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── tasks
│   └── main.yml
├── templates
└── vars
    └── main.yml

Prepare Unit Test

Create tests directory and prepare a ansible playbook named test.yml as follows:

- hosts: 127.0.0.1
  connection: local
  tags:
    - case-1
  vars:
    ansible_unit_test: True
  roles:
    - role: ../..

First two lines are to run Ansible playbook on the local system.

- hosts: 127.0.0.1
  connection: local

Point 1 Add a tag named case-1 as a test case name (case-1is just sample name).

tags:
  - case-1

Point 2 Define ansible_unit_test as a environment variable.

vars:
  ansible_unit_test: True

The variable is used to skip ansible tasks which you don't want to run while unit testing. condition.

Next, add other depending Ansible roles to tests/requirements.yml file.

- src: tumf.systemd-service

Install depending roles by ansible-galaxy command:

$ ansible-galaxy install -r tests/requirements.yml -p tests/roles

How to create testable roles

Setup out path prefix and set skip tasks to run unit tests.

Setup out path prefix

To generate all files under tests/cases/{name of test case} directory, prefix all file paths with prefix_dir variables.

First, declare 'prefix_dir' in the 'defaults/main.yml' file.

prefix_dir: ""

Next, prefix all file paths (in the dest of template and so on) with prefix_dir as follows:

- template: src="default.j2" dest="{{ prefix_dir }}/etc/default/sample.j2"
  notify: reload systemd

Skip tasks

Put when: is not ansible_unit_test condition to skip some tasks.
(In this case, ansible-playbook w/ -C options is not works because your develop environment may be different from production
environment.)

For example:

- service: name="sample" state=started enabled=yes
  when: not ansible_unit_test

To set ansible_unit_test to False as default.

ansible_unit_test: False

Test running script

Put test runner script to tests/run as follows:

#!/bin/bash

usage_exit() {
        echo "Usage: $0 [-w] name" 1>&2
        exit 1
}

check="-C"
while getopts wh option
do
    case $option in
        w)
            check=""
            ;;
        h)
            usage_exit
            ;;
    esac
done

shift $((OPTIND - 1))

mkdir -p tests/cases
cases=$(ls tests/cases)
if [ ! -z $1 ];then
    cases=$1
fi
errors=0

for case in $cases
do
    out=$(ansible-playbook ./tests/test.yml -i 127.0.0.1, -t $case -D $check -e prefix_dir="cases/${case}")
    result=$?
    if echo $out|tail -n 1 |grep -E "changed=0\s+unreachable=0\s+failed=0" >/dev/null
    then
        echo -n "."
    else
        echo $case
        echo
        echo "$out"
        errors=$(( errors+1 ))
    fi
done

if [ $errors -eq 0 ]
then
    echo " ok"
else
    echo "${errors} error(s)"
fi
exit $errors

Create test cases

To create unit tests as follows:

$ ./tests/run case-1            
case-1


PLAY [127.0.0.1]
GATHERING FACTS ***************************************************************

ok: [127.0.0.1]

TASK: [../.. | template src="default.j2" dest="{{ prefix_dir }}/etc/default/sample"] ***
--- before: cases/case-1/etc/default/sample
+++ after: /Users/tumf/tmp/sample/templates/default.j2
@@ -0,0 +1 @@
+default test

changed: [127.0.0.1]

TASK: [../.. | service name="sample" state=started enabled=yes] ***************
skipping: [127.0.0.1]

NOTIFIED: [../.. | reload systemd] ********************************************
skipping: [127.0.0.1]

PLAY RECAP ********************************************************************
127.0.0.1                  : ok=2    changed=1    unreachable=0    failed=0   
1 error(s)

case-1 is a test case to be set in tests/test.yml file.
Fix playbook until to generate correct templates/default.j2 file.

Register test case

To register tests/cases/case-1/etc/default/sample as a test case, run command as follows:

$ ./tests/run -w case-1

This command generates tests/cases/case-1/etc/default/sample as a test case,then you can run unit test as follows:

$ ./tests/run case-1   
. ok

Note You can run ./tests/run without the test case name to run all tests in the tests/cases directory.

$ ./tests/run
. ok

Add test cases

You can add some test cases if you want.
Simple examples as follows:

---
- hosts: 127.0.0.1
  connection: local
  tags:
    - case-1
  vars:
    ansible_unit_test: True
    var: var of case-1
  roles:
    - role: ../..
- hosts: 127.0.0.1
  connection: local
  tags:
    - case-2
  vars:
    ansible_unit_test: True
    var: var of case-2
  roles:
    - role: ../..

Developing with test until all tests pass.

$ ./tests/run case-2

Register as a test case if it works.

$ ./tests/run -w case-2

You can run all tests (both case-1 and case-2) to use following command:

$ ./tests/run           
.. ok

It's easy :)

Try to raise errors

If you change var, the tests will fail.

Fix the playbook as follows:

vars:
  ansible_unit_test: True
  var: var of case-one # change here

Run tests then report an error.

$ ./tests/run
case-1


PLAY [127.0.0.1] **************************************************************

GATHERING FACTS ***************************************************************

ok: [127.0.0.1]

TASK: [../.. | file state="directory" path="{{ prefix_dir }}/etc/default"] ****

ok: [127.0.0.1]

TASK: [../.. | template src="default.j2" dest="{{ prefix_dir }}/etc/default/sample"] ***
--- before: cases/case-1/etc/default/sample
+++ after: /Users/tumf/tmp/sample/templates/default.j2
@@ -1 +1 @@
-default test var of case-1
+default test var of case-one

changed: [127.0.0.1]

TASK: [../.. | service name="sample" state=started enabled=yes] ***************
skipping: [127.0.0.1]

NOTIFIED: [../.. | reload systemd] ********************************************
skipping: [127.0.0.1]

PLAY RECAP ********************************************************************
127.0.0.1                  : ok=3    changed=1    unreachable=0    failed=0   
.1 error(s)

Register the test case using ./tests/run -w case-1 command if the report comes up to your expectations.

In such a way, you can develop ansible playbook with unit testing.

Using Travis CI

Create .travis-ci.yml as follows to integrate Travis CI.

language: python
python:
- '2.7'
install:
- pip install ansible
#- ansible-galaxy install -r tests/requirements.yml -p tests/roles
before_script:
- ansible --version
- ansible-playbook --syntax-check ./tests/test.yml -i ./tests/hosts
script:
- ./tests/run

Uncommenting following line if the playbook depends on other playbooks.

#- ansible-galaxy install -r tests/requirements.yml -p tests/roles

1 Response
Add your response

Yoshihiro,

It's a nice post : very useful ! Thank you.
Maybe you should publish your template on Github.

Regards,

François, from Paris.

over 1 year ago ·