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-1
is 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 thetests/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
Written by Yoshihiro TAKAHARA
Related protips
1 Response
Yoshihiro,
It's a nice post : very useful ! Thank you.
Maybe you should publish your template on Github.
Regards,
François, from Paris.