4 min read

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.

  1. Make sure to have installed Ansible

  2. Make sure to have installed VirtualBox

  3. Make sure to have installed Vagrant

  4. Make sure to have installed Ruby

  5. Create role using Ansible Galaxy tool:

    ansible-galaxy init johnroach.jenkins
    
  6. Change directory into johnroach.jenkins

  7. Install kitchen-ansible:

    gem install kitchen-ansible
    
  8. Install kitchen-vagrant (kitchen-docker if you ar eto use docker):

    gem install kitchen-vagrant
    
  9. Install bundler to bundle all these installations and make sure all this is repeatable:

    gem install bundler
    
  10. Initialize kitchen:

    kitchen init --driver=vagrant --provisioner=ansible --create-gemfile
    
  11. Get rid of the ansible-galaxy generated tests directory and leave the kitchen generated test directory

  12. 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"
    
  13. Run bundle install:

    bundle install
    

    You will want to keep the Gemfile.lock file

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

  15. 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
    
  16. 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
    
  17. 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:

asciicast

And the code looks like something like this: https://github.com/JohnRoach/johnroach.jenkins