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:

1
2
3
4
5
6
7
8
9
10
11
12
13
├── 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:

1
2
3
4
5
6
7
8
- 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.

1
2
- 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).

1
2
tags:
- case-1

Point 2 Define ansible_unit_test as a environment variable.

1
2
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.

1
- 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.

1
prefix_dir: ""

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

1
2
- 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:

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

To set ansible_unit_test to False as default.

1
ansible_unit_test: False

Test running script

Put test runner script to tests/run as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ ./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:

1
$ ./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:

1
2
$ ./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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
- 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.

1
$ ./tests/run case-2

Register as a test case if it works.

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

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

1
2
$ ./tests/run
.. ok

It’s easy :)

Try to raise errors

If you change var, the tests will fail.

Fix the playbook as follows:

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

Run tests then report an error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ ./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.

1
2
3
4
5
6
7
8
9
10
11
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.

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