Test Driven Development with Infrastructure Code
TDD(Test Driven Development) IS possible with IaC (Infrastructure as Code). Especially if you use Ansible!
Ansible roles can be tested using a test tool kitchen-ansible which was built using test kitchen.
How you say? Well let us go through the motions step by step for a Vagrant installation. You can use Docker too. However if your roles requires to things with systemd
you must be aware that a lot of Docker containers don't come with that for good reason. I will see if I can post a Docker role for next time.
-
Make sure to have installed Ansible
-
Make sure to have installed VirtualBox
-
Make sure to have installed Vagrant
-
Make sure to have installed Ruby
-
Create role using Ansible Galaxy tool:
ansible-galaxy init johnroach.jenkins
-
Change directory into
johnroach.jenkins
-
Install kitchen-ansible:
gem install kitchen-ansible
-
Install kitchen-vagrant (kitchen-docker if you ar eto use docker):
gem install kitchen-vagrant
-
Install bundler to bundle all these installations and make sure all this is repeatable:
gem install bundler
-
Initialize kitchen:
kitchen init --driver=vagrant --provisioner=ansible --create-gemfile
-
Get rid of the ansible-galaxy generated
tests
directory and leave the kitchen generatedtest
directory -
Fix the Gemfile that got generated:
source "https://rubygems.org" gem "json" gem "kitchen-ansible" gem "kitchen-vagrant" gem "kitchen-verifier-serverspec" gem "serverspec" gem "test-kitchen"
-
Run bundle install:
bundle install
You will want to keep the
Gemfile.lock
file -
Configure the
.kitchen.yml
file that was generated when you initialized kitchen. It should looks something like this:--- driver: name: vagrant verifier: name: serverspec default_pattern: true provisioner: name: ansible hosts: localhost ansible_cfg_path: test/ansible.cfg require_chef_for_busser: false require_ruby_for_busser: false ansible_host_key_checking: false additional_copy_role_path: - test/roles/williamyeh.oracle-java platforms: - name: centos-7.2 suites: - name: default provisioner: hosts: all name: ansible_playbook playbook: test/integration/default/test.yml additional_copy_path: - "."
I would like to talk about some of the key components in this file. first let us look at this:
driver: name: vagrant
This means that the test will use vagrant to spin up a virtual machine to run the the tests.
platforms: - name: centos-7.2
This means that vagrant will pull down a centos-7.2 box image for this vm to use.
verifier: name: serverspec default_pattern: true
This means as a verifier we will be using serverspec. You can also use busser-serverspec if you wish. But for this type of setup i have found serverspec verifier to be better and well documented.
provisioner: ...
The provisioner portion is all about setting up the provisioner(here Ansible). The one thing that might be interesting is the role path. This is the role path which where the
requirements.yml
file will be used(the final structure of the example will be at the end).suites: ...
Suites section is meant to hold the definitions needed for the test suites. Pretty straight forward.
-
Let us create the files that we are point to in the
.kitchen.yml
file. First let us create the test.yml file(test/integration/default/test.yml
):--- - hosts: localhost roles: - jenkins
Now let us create the ansible.cfg file(
test/ansible.cfg
):[defaults] ansible_host_key_checking = False
Let us create the
test/requirements.yml
file:- src: williamyeh.oracle-java
Let us create the test runner
test.sh
in the root directory:ansible-galaxy install -r test/requirements.yml -p test/roles kitchen test
Make sure to make this file executable:
chmod +x test.sh
The directory structure should now look like the following:
. ├── chefignore ├── defaults │ └── main.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── meta │ └── main.yml ├── README.md ├── tasks │ └── main.yml ├── templates ├── test │ ├── ansible.cfg │ ├── integration │ │ └── default │ │ ├── serverspec │ │ │ └── jenkins_repo_spec.rb │ │ └── test.yml │ ├── requirements.yml │ └── roles ├── test.sh └── vars └── main.yml
-
In good TDD fashion let us now write a failing test for our Jenkins role. Place the spec file in
test/integration/default/serverspec
. Make sure to name the file with the ending_spec.rb
. This will allow the test kitchen to pull in the test. A sample test looks like something below(jenkins_repo_spec.rb
):require 'serverspec' set :backend, :exec describe file('/etc/yum.repos.d/jenkins.repo') do it { should exist } it { should be_file } end
-
The tests should fail!!! You should get some similar result to:
File "/etc/yum.repos.d/jenkins.repo" should exist (FAILED - 1) should be file (FAILED - 2) Failures: 1) File "/etc/yum.repos.d/jenkins.repo" should exist Failure/Error: it { should exist } expected File "/etc/yum.repos.d/jenkins.repo" to exist /bin/sh -c test\ -e\ /etc/yum.repos.d/jenkins.repo # /tmp/verifier/suites/serverspec/jenkins_repo_spec.rb:6:in `block (2 levels) in <top (required)>' 2) File "/etc/yum.repos.d/jenkins.repo" should be file Failure/Error: it { should be_file } expected `File "/etc/yum.repos.d/jenkins.repo".file?` to return true, got false /bin/sh -c test\ -f\ /etc/yum.repos.d/jenkins.repo # /tmp/verifier/suites/serverspec/jenkins_repo_spec.rb:7:in `block (2 levels) in <top (required)>' Finished in 0.05276 seconds (files took 0.24741 seconds to load) 2 examples, 2 failures
Don't worry this is expected!! You want your tests to fail first in TDD world!! What you do next is you start fixing it. And running it again!
Now since this article really isn't about writing Ansible code I am going to leave the actual writing of the code to you! However the end result looks like something like this:
And the code looks like something like this: https://github.com/JohnRoach/johnroach.jenkins