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
testsdirectory and leave the kitchen generatedtestdirectory -
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 installYou will want to keep the
Gemfile.lockfile -
Configure the
.kitchen.ymlfile 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: vagrantThis means that the test will use vagrant to spin up a virtual machine to run the the tests.
platforms: - name: centos-7.2This means that vagrant will pull down a centos-7.2 box image for this vm to use.
verifier: name: serverspec default_pattern: trueThis 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.ymlfile 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.ymlfile. First let us create the test.yml file(test/integration/default/test.yml):--- - hosts: localhost roles: - jenkinsNow let us create the ansible.cfg file(
test/ansible.cfg):[defaults] ansible_host_key_checking = FalseLet us create the
test/requirements.ymlfile:- src: williamyeh.oracle-javaLet us create the test runner
test.shin the root directory:ansible-galaxy install -r test/requirements.yml -p test/roles kitchen testMake sure to make this file executable:
chmod +x test.shThe 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 failuresDon'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
