Integration testing with ServerSpec

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!

Note

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.

Getting ready

To step through this recipe, you will need the following:

  • A working Chef DK installation on the workstation
  • A working Vagrant installation on the workstation
  • The Chef code from Chapter 6, Fundamentals of Managing Servers with Chef and Puppet, or any custom Chef code

How to do it…

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

Creating a ServerSpec helper script

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'

Testing a package installation

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!

Note

While writing integration tests, we strongly suggest that you use Test Kitchen to create/converge/set up/verify the sequence and not the simple kitchen test command that does everything at once—the manual way is much faster!

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

Testing for service status

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

Testing for listening ports

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

Testing for files existence and content

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

Testing for repository existence

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.

There's 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:

  • Installing Apache using our class.
  • Verifying Puppet applies properly.
  • Verifying that a second run of Puppet does not change anything: we want to prove the code is idempotent.

Let's try the test!

$ 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:

  • Install apache
  • Define a virtual host
  • Create the root directory of the virtual host
  • Create a htpasswd file with a test user
  • Create a .htaccess file in the root directory, using the previous htpasswd file

Regarding tests, we want to:

  • Verify Puppet applies
  • Verify the code is idempotent
  • Verify apache is running and activated at boot
  • Verify apache is listening
  • Verify the virtual host is deployed and activated with the correct DocumentRoot
  • Verify the htpasswd file is deployed with a correct content
  • Verify the .htaccess file is deployed with a correct content

The 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!

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.151.26