Integration testing comes after unit testing: we're now testing the actual functionality on a real black box system. We're probably using many cookbooks that are doing a lot of things, each unit tested in an early stage, but how are they playing together for real? Everything assembled together, intentions might match, but reality can be very different. Overrides might overlap, a forgotten recipe can change behavior, a service might not start and then changes will happen, regression can be introduced, or newer systems or updates can break; there are countless reasons why things can go wrong at a certain point on a real system. That's the reason we need integration testing; testing the outcome of the combination of all our cookbooks applied to a real test system, and now.
In the case of Chef, we have a great tool to help us for this matter named Test Kitchen, which we previously installed and configured to run and execute tests. Let's now write these tests!
We'll write integrations tests for the mysite
cookbook written in Chapter 6, Fundamentals of Managing Servers with Chef and Puppet, for demonstration purposes, but those are completely generic and can be reused anywhere. We'll test for services, files, directories, yum repositories, packages, ports, and injected content. This way, we'll be certain that the code we're writing actually does what it's expected to do in the (simulated) real world!
We strongly suggest that you add those integrations tests to an automated CI system. So that after a change in the code, tests can be automatically launched and as time go by, complexity soars with many cases added, so you just don't have to think about it: it's all going to be tested, and if your change breaks something you missed, you'll know it in seconds. Nobody wants to manually verify that nothing breaks on three versions of four operating systems at each change.
To step through this recipe, you will need the following:
Depending on how the cookbooks we test are created, a test
folder can be created with some sample content under it. We don't need it, so be sure to get rid of everything under the test
folder to start fresh. We'll use the mysite
cookbook from Chapter 6, Fundamentals of Managing Servers with Chef and Puppet, as the base cookbook to build our ServerSpec tests on, but obviously those tests can be used anywhere:
$ cd cookbooks/mysite $ rm -rf test/*
Test Kitchen works with test suites, and consequently expects a folder hierarchy with the same name as the suite name, in an integration
folder. The final folder hierarchy for a default
test suite will then be mysite/test/integration/default/serverspec
.
$ mkdir -p test/integration/default/serverspec
ServerSpec needs a minimum of two lines of configuration that must be repeated on each test. Instead of repeating ourselves, let's create a helper script in test/integration/default/serverspec/spec_helper.rb
:
require 'serverspec' # Required by serverspec set :backend, :exec
Now all our tests will just need to include the following at the top of the file:
require 'spec_helper'
Our cookbooks are doing a lot of things, and among the most important things is package installation. These things were unit tested previously, but now we're in integration. Are those packages really installed? Let's find out by writing the test for the httpd
package in apache_spec.rb
:
require 'spec_helper' describe package('httpd') do it { should be_installed } end
We can now fire up Test Kitchen and see if this specific package is really installed!
Similarly, testing for the php
packages in a php_spec.rb
file will look exactly the same:
require 'spec_helper' describe package('php') do it { should be_installed } end describe package('php-cli') do it { should be_installed } end describe package('php-mysql') do it { should be_installed } end
ServerSpec allows us to test the actual process status. In the recipe to install the Apache HTTPD server, we requested it to be enabled and running. Let's find out if it's really the case by adding the following to the apache_spec.rb
file:
describe service('httpd') do it { should be_enabled } it { should be_running } end
In the case of our MySQL installation, the documentation from the official cookbook indicates the service is by default named mysql-default
(and not the usual mysqld
). In a mysql_spec.rb
file, add the following:
describe service('mysql-default') do it { should be_enabled } it { should be_running } end
ServerSpec is a great tool to test listening ports. In our case, we expect Apache to listen on port 80
(HTTP) and we configured MySQL to listen to 3306
. Add the following to the apache_spec.rb
file:
describe port('80') do it { should be_listening } end
Similarly, add the following for MySQL in the mysql_spec.rb
file:
describe port('3306') do it { should be_listening } end
We previously unit tested the intention to create all those files in our cookbooks, such as a VirtualHost with a custom name, impacting both filename and content (that's what the mysite
cookbook from Chapter 6, Fundamentals of Managing Servers with Chef and Puppet, does, override the defaults from the custom apache cookbook). Is it really working? Let's find out by testing our virtual hosting configuration with vhost_spec.rb
:
describe file('/etc/httpd/conf.d/mysite.conf') do it { should exist } it { should be_mode 644 } its(:content) { should match /ServerName mysite/ } it { should be_owned_by 'root' } it { should be_grouped_into 'root' } end
This actually proves the default attribute really got overridden by the mysite
value, and the content of the virtual host configuration file also matches this value. The cookbook really works.
A directory can similarly be tested like this in the same vhost_spec.rb
file:
describe file('/var/www/mysite') do it { should be_directory } end
Another interesting test to be done is to check the content of the htpasswd
file; in Chapter 6, Fundamentals of Managing Servers with Chef and Puppet, we wrote a recipe making a request to the Chef server for authorized users in a data bag. We unit tested the feature by stubbing the data bag, and then using Test Kitchen, we configured it to simulate the availability of those data bags. Is this Chef Server-specific code really working and adding the john
user in the htpasswd
file while restricting access to it? Let's find out by adding the following to an htaccess_spec.rb
file:
describe file('/etc/httpd/htpasswd') do it { should exist } it { should be_mode 660 } its(:content) { should match /john/ } it { should be_owned_by 'root' } it { should be_grouped_into 'root' } end
Our mysite
cookbook example from Chapter 6, Fundamentals of Managing Servers with Chef and Puppet, is using the official Chef cookbook to deploy MySQL, and that includes adding a yum repository. As it's now an important part of the system, we'd better test for its existence and status! To test a yum repository, add the following to the mysql_spec.rb
file:
describe yumrepo('mysql57-community') do it { should be_exist } it { should be_enabled } end
Many other parts of a system can be tested using ServerSpec, notably in networking (routing tables, gateways, and interfaces), Unix users and groups, real commands, cron jobs, and many more.
Using Puppet and Beaker, let's try to write acceptance tests for our Apache module. Acceptance tests needs to be placed in the spec/acceptance
directory.
We need to define a helper file that will be shared by all acceptance tests. Let's create a spec/spec_helper_acceptance.rb
file with the following content:
require 'beaker-rspec' require 'beaker/puppet_install_helper' # Install puppet run_puppet_install_helper RSpec.configure do |c| # Project root proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) # Output should contain test descriptions c.formatter = :documentation # Configure nodes c.before :suite do # Install module puppet_module_install(:source => proj_root, :module_name => 'apache') end end
This helper file will be used to install Puppet on the test box, and populate the module directory with our apache
module.
As a first basic acceptance test for the main apache
class, let's create spec/acceptances/classes/apache_spec.rb
, with the following content:
require 'spec_helper_acceptance' describe 'Apache' do describe 'Puppet code' do it 'should compile and work with no error' do pp = <<-EOS class { 'apache': } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) end end end
The goals of this test are as follows:
$ rake beaker ... ... Beaker::Hypervisor, found some vagrant boxes to create Bringing machine 'ubuntu-1604-x64' up with 'virtualbox' provider... ... ... Apache Puppet code localhost $ scp /var/folders/k9/7sp85p796qx7c22btk7_tgym0000gn/T/beaker20161101-75828-1of1g5j ubuntu-1604-x64:/tmp/apply_manifest.pp.cZK277 {:ignore => } localhost $ scp /var/folders/k9/7sp85p796qx7c22btk7_tgym0000gn/T/beaker20161101-75828-1l28bth ubuntu-1604-x64:/tmp/apply_manifest.pp.q2Z81Z {:ignore => } should compile and work with no error Destroying vagrant boxes ==> ubuntu-1604-x64: Forcing shutdown of VM... ==> ubuntu-1604-x64: Destroying VM and associated drives... Finished in 19.68 seconds (files took 1 minute 20.11 seconds to load) 1 example, 0 failures
In this example, Beaker created the box, installed Puppet, uploaded our code, applied Puppet twice to validate our test, and destroyed the box.
To have more logs regarding Puppet agent installation and execution, we can add a line log_level: verbose
in the nodeset
file:
HOSTS:
ubuntu-1604-x64:
roles:
- agent
- default
platform: ubuntu-16.04-amd64
hypervisor: vagrant
box: bento/ubuntu-16.04
CONFIG:
type: foss
log_level: verbose
Now let's extend our test to use all code contained in the apache module. We want to update the manifest at the top of the file in order to do the following:
htpasswd
file with a test user.htaccess
file in the root directory, using the previous htpasswd
fileRegarding tests, we want to:
DocumentRoot
htpasswd
file is deployed with a correct content.htaccess
file is deployed with a correct contentThe updated acceptance test code is now as follows:
require 'spec_helper_acceptance' describe 'Apache' do describe 'Puppet code' do it 'should compile and work with no error' do pp = <<-EOS class { 'apache': } apache::vhost{'mysite': website => 'www.sample.com', docroot => '/var/www/docroot', } apache::htpasswd{'htpasswd': filepath => '/etc/apache2/htpasswd', users => [ { "id" => "user1", "htpasswd" => "hash1" } ], } file { '/var/www/docroot': ensure => directory, owner => 'www-data', group => 'www-data', mode => '0755', } apache::htaccess{'myhtaccess': filepath => '/etc/apache2/htpasswd', docroot => '/var/www/docroot', } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) end end # Apache running and enabled at boot ? describe service('apache2') do it { is_expected.to be_enabled } it { is_expected.to be_running } end # Apache listening ? describe port(80) do it { is_expected.to be_listening } end # Vhost deployed ? describe file ('/etc/apache2/sites-available/www.sample.com.conf') do its(:content) { should match /DocumentRoot /var/www/docroot/ } end describe file ('/etc/apache2/sites-enabled/www.sample.com.conf') do it { is_expected.to be_symlink } end # htpasswd file deployed ? describe file ('/etc/apache2/htpasswd') do its(:content) { should match /user1:hash1/ } end # htaccess file deployed ? describe file ('/var/www/docroot/.htaccess') do its(:content) { should match /AuthUserFile /etc/apache2/htpasswd/ } end end
Now, let's try to run Beaker again:
$ rake beaker … … Beaker::Hypervisor, found some vagrant boxes to create Bringing machine 'ubuntu-1604-x64' up with 'virtualbox' provider... … … Apache Puppet code localhost $ scp /var/folders/k9/7sp85p796qx7c22btk7_tgym0000gn/T/beaker20161103-41882-1twwbr2 ubuntu-1604-x64:/tmp/apply_manifest.pp.nWPdZJ {:ignore => } localhost $ scp /var/folders/k9/7sp85p796qx7c22btk7_tgym0000gn/T/beaker20161103-41882-73vqlb ubuntu-1604-x64:/tmp/apply_manifest.pp.0Jht7j {:ignore => } should compile and work with no error Service "apache2" should be enabled should be running Port "80" should be listening File "/etc/apache2/sites-available/www.sample.com.conf" content should match /DocumentRoot /var/www/docroot/ File "/etc/apache2/sites-enabled/www.sample.com.conf" should be symlink File "/etc/apache2/htpasswd" content should match /user1:hash1/ File "/var/www/docroot/.htaccess" content should match /AuthUserFile /etc/apache2/htpasswd/ Destroying vagrant boxes ==> ubuntu-1604-x64: Forcing shutdown of VM... ==> ubuntu-1604-x64: Destroying VM and associated drives... Finished in 20.22 seconds (files took 1 minute 24.54 seconds to load) 8 examples, 0 failures
We now have a complete acceptance test suite for our Apache module!
3.145.151.26