Image for post
Image for post

Testing Chef Cookbooks with InSpec

Integration Testing using Test Kitchen and Inspec

For integration testing of systems with Chef, you can use the tool InSpec, either directly to drive tests, or use the test harness Test Kitchen. The advantage of using the test harness is that it can setup the systems, configure them, and then test them with InSpec. With InSpec alone, so you would have to create the systems and configure them beforehand.

Prerequisites

For integration testing journey with Chef, you will need the following prerequisites:

Previous Guides

I have written some previous guides that show how to get these prerequisites for macOS, Windows, and Linux.

Install Test Kitchen and InSpec with Ruby (Advanced)

As an alternative to installing ChefDK, for more advanced scenarios where you may manage your own Ruby environment, you can install the required tools with the Gemfile below.

source :rubygemsgem 'test-kitchen'
gem 'kitchen-vagrant'
gem 'kitchen-inspec'
gem 'inspec'

Part I: Getting Started

The instructions in this part will create small project area and our basic Chef cookbook.

Creating Chef Cookbook

Create the basic structure and the populate the cookbook:

PROJ_PATH=${HOME}/vagrant-chef
COOKBOOK_PATH=${PROJ_PATH}/cookbooks/hello_web
mkdir -p ${COOKBOOK_PATH}/{files/default,attributes,recipes}
cd ${COOKBOOK_PATH}
##### Create Cookbook Attributes
cat <<-'ATTRIB' > attributes/default.rb
# Dynamic Attributes based on Platform Family
case node['platform_family']
when 'rhel'
default['hello_web']['package'] = 'httpd'
default['hello_web']['service'] = 'httpd'
default['hello_web']['docroot'] = '/var/www/html'
when 'debian'
default['hello_web']['package'] = 'apache2'
default['hello_web']['service'] = 'apache2'
default['hello_web']['docroot'] = '/var/www/html'
when 'freebsd'
default['hello_web']['package'] = 'apache24'
default['hello_web']['service'] = 'apache24'
default['hello_web']['docroot'] = '/usr/local/www/apache24/data/'
else
default['hello_web']['package'] = nil
default['hello_web']['docroot'] = nil
default['hello_web']['service'] = nil
end
ATTRIB
##### Create Cookbook Recipe
cat <<-'RECIPE' > recipes/default.rb
apt_update 'Update the apt cache daily' do
frequency 86_400
action :periodic
end
package node['hello_web']['package']cookbook_file "#{node['hello_web']['docroot']}/index.html" do
source 'index.html'
action :create
end
service node['hello_web']['service'] do
supports status: true, restart: true, reload: true
action %i(enable start)
end
RECIPE
##### Create Cookbook Metadata
cat <<-'METADATA' > metadata.rb
name 'hello_web'
version '0.0.2'
chef_version '>= 12.14' if respond_to?(:chef_version)
METADATA
##### Create Content
cat <<-'HTML' > files/default/index.html
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
HTML
~/vagrant-chef
└── cookbooks
└── hello_web
├── attributes
│ └── default.rb
├── files
│ └── default
│ └── index.html
├── metadata.rb
└── recipes
└── default.rb

Creating Test Testing Structure

Create the basic structure for Test Kitchen and InSpec:

PROJ_PATH=${HOME}/vagrant-chef
COOKBOOK_PATH=${PROJ_PATH}/cookbooks/hello_web
INSPEC_PATH=${COOKBOOK_PATH}/test/integration/default/
cd ${COOKBOOK_PATH}mkdir -p ${INSPEC_PATH}
touch .kitchen.yml ${INSPEC_PATH}/default_test.rb
~/vagrant-chef
└── cookbooks
└── hello_web
├── .kitchen.yml
├── attributes
│ └── default.rb
├── files
│ └── default
│ └── index.html
├── metadata.rb
├── recipes
│ └── default.rb
└── test
└── integration
└── default
└── default_test.rb

Create Test Kitchen Configuration

Update the .kitchen.yml with the following:

---
driver:
name: vagrant
provisioner:
name: chef_zero
always_update_cookbooks: true
verifier:
name: inspec
platforms:
- name: ubuntu-16.04
suites:
- name: default
run_list:
- recipe[hello_web::default]

Creating Inspec Tests

Let’s create our test script using InSpec. We have two parts, one that tests the configuration, which is not considered a good practice, but may be required for edge use cases and put here for illustrative purposes, and another that tests what is the purpose of our cookbook has promised to complete.

######## TEST CONFIGURATION ########
describe package('apache2') do
it { should be_installed }
end
describe service('apache2') do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
######## TEST PROMISE ########
describe port(80) do
it { should be_listening }
end
describe http('localhost') do
its('status') { should eq 200 }
its('headers.Content-Type') { should include 'text/html' }
its('body') { should include 'Hello World!' }
end

Running The Tests

Now that Test Kitchen is configured and we have our tests, let’s try this out in separate steps.

kitchen create    # create the system
kitchen converge # download chef and provision the system
Recipe: hello_web::default
* apt_package[apache2] action install
- install version 2.4.18-2ubuntu3.9 of package apache2
* service[apache2] action enable (up to date)
* service[apache2] action start (up to date)
kitchen verify    # run inspec to test system
Version: (not specified)
Target: ssh://vagrant@127.0.0.1:2202
System Package apache2
✔ should be installed
Service apache2
✔ should be installed
✔ should be enabled
✔ should be running
Port 80
✔ should be listening
http GET on localhost
✔ status should eq 200
✔ headers.Content-Type should include "text/html"
✔ body should include "Hello World!"
Test Summary: 8 successful, 0 failures, 0 skipped
Finished verifying <default-ubuntu-1604> (0m0.43s).

Part II: Running More Platforms

Let’s add some new platforms to try out. Update the .kitchen.yml with the following:

---
driver:
name: vagrant
provisioner:
name: chef_zero
always_update_cookbooks: true
verifier:
name: inspec
platforms:
- name: ubuntu-16.04
- name: centos-7
- name: freebsd-11.2
suites:
- name: default
run_list:
- recipe[hello_web::default]

Create and Configure New Systems

Take this out for a spin with:

kitchen create    # create the system
kitchen converge # download chef and provision system

Verify New Systems

Also now that the systems work, do a verification:

kitchen verify    # run inspec to test system
System Package apache2
× should be installed
expected that `System Package apache2` is installed
Service apache2
× should be installed
expected that `Service apache2` is installed
× should be enabled
expected that `Service apache2` is enabled
× should be running
expected that `Service apache2` is running

Update Inspec Tests

One way we can have more flexibility in the package and service name is by supporting InSpec attributes (not to be confused with Chef attributes, totally unrelated).

######## ATTRIBUTES ########
PACKAGE = attribute('package',
default: 'apache2',
description: 'package name'
)

SERVICE = attribute('service',
default: 'apache2',
description: 'service name'
)
######## TEST CONFIGURATION ########
describe package(PACKAGE) do
it { should be_installed }
end
describe service(SERVICE) do
it { should be_installed }
it { should be_enabled }
it { should be_running }
end
######## TEST PROMISE ########
describe port(80) do
it { should be_listening }
end
describe http('localhost') do
its('status') { should eq 200 }
its('headers.Content-Type') { should include 'text/html' }
its('body') { should include 'Hello World!' }
end
---
driver:
name: vagrant
provisioner:
name: chef_zero
always_update_cookbooks: true
verifier:
name: inspec
platforms:
- name: ubuntu-16.04
- name: centos-7
verifier:
attributes:
package: httpd
service: httpd
- name: freebsd-11.2
verifier:
attributes:
package: apache24
service: apache24
suites:
- name: default
run_list:
- recipe[hello_web::default]

Run the Tests

kitchen verify
System Package httpd
✔ should be installed
Service httpd
✔ should be installed
✔ should be enabled
✔ should be running
System Package apache24
↺ The `package` resource is not supported on your OS yet.
Service apache24
✔ should be installed
✔ should be enabled
✔ should be running

Part III: Better Practices

We can see two things we’ve learned so far. First, we know how we can pass attributes from Test Kitchen to our InSpec tests, which is a useful facility. Secondly, we can see how testing specific configuration is expensive than just testing the promise. Testing the configuration should only be used for niche use cases, and generally just avoided.

Update Tests

Update the test (default_test.rb) to include only the promise.

######## TEST PROMISE ########
describe port(80) do
it { should be_listening }
end
describe http('localhost') do
its('status') { should eq 200 }
its('headers.Content-Type') { should include 'text/html' }
its('body') { should include 'Hello World!' }
end

Update Kitchen Configuration

Update the Test Kitchen configuration (.kitchen.yml) to remove passing InSpec attributes as we no longer need this:

driver:
name: vagrant
provisioner:
name: chef_zero
always_update_cookbooks: true
verifier:
name: inspec
platforms:
- name: ubuntu-16.04
- name: centos-7
- name: freebsd-11.2
suites:
- name: default
run_list:
- recipe[hello_web::default]

Run the tests

kitchen verify

Wrap Up

That’s all there is to it for testing easily testing cookbooks using integration testing, and how to use InSpec attributes should you need it.

Written by

Linux NinjaPants Automation Engineering Mutant — exploring DevOps, Kubernetes, CNI, IAC

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store