Tuesday 3 January 2017

Chapter 16. Testing

In this final chapter of the book, we will cover how to test your Chef automation code. It is important to perform testing and verification steps before deploying your Chef code to production to ensure it works as intended.
In nearly all the exercises in this book, we’ve taken care to add sections highlighting how to manually verify that your Chef code is working properly. In this chapter, we’ll show you how to automate this process.

Testing Rationale

Using a configuration management tool like Chef gets you 50% of the way there in automating testing and verification. Because Chef automates infrastructure in a repeatable manner, it inherently makes apps running in these environments more testable. This is why we introduced Test Kitchen so early in the book, in Chapter 5, so that you could see this in action. Throughout the book, you’ve deployed your Chef code to a local development sandbox environment. Using Test Kitchen should give you confidence that if you deployed the same code to a production Chef environment, it will behave in the same manner.
The other 50% of the testing rationale, besides using Chef in the first place, is to be strategic when testing and deploying automation code. Just as it is best to introduce change to application code in small batches, it is also best to introduce change to your infrastructure in small batches. As Figure 16-1 shows, you should work in short bursts, performing a short build-test-deploy cycle. This approach can help ensure that enough testing gets done to result in an infrastructure and application of high quality.
Using short build→test→deploy cycles
Figure 16-1. Using short build→test→deploy cycles
It’s harder to estimate when long, drawn-out development cycles will complete than shorter “baby step” development increments as advocated in Figure 16-1. As Figure 16-2 shows, what almost inevitably happens with long, drawn-out development cycles is that testing is done at the last minute and it ultimately takes longer to deliver a large feature than intended. At the same time, the shorter testing period sacrifices quality, and this can lead to the “throw it over the wall to operations” scenario where applications work fine in development but don’t work in production. Configuration management tools like Chef try to address this problem.
Testing crunches in lengthy development projects
Figure 16-2. Testing crunches in lengthy development projects
Further, there is a monetary cost associated with finding and fixing bugs in your software and infrastructure code, and that cost goes up the further out in the development cycle you discover issues. Figure 16-3 shows the relative cost of fixing a software bug as development progresses. Your infrastructure code is no different than application code in this respect:
  • Requirements
  • Design
  • Coding
  • Development testing
  • Acceptance testing
  • Operations/production
Bugfix costs as a function of the development stage
Figure 16-3. Bugfix costs as a function of the development stage
Finding and fixing issues during the requirements and design phase is not very expensive. However, the cost goes up the further out in the development lifecycle you go. When you wait to find bugs after they have been coded, the costs are 20 times to 50 times greater than if you had caught them earlier. During the production phase, the cost is 150 times greater.
This graphic serves as a good reminder of how you should always approach a Chef coding project:
  1. Code right the first time, because it costs a lot more to fix things later.
  2. Find bugs and issues as early as possible, ideally before they ever get in, or at least as close to the time of coding as possible.
  3. Make changes in small batches—the smaller the change, the less likely you are to introduce a lot of new defects. It’s also easier to test in small batches.
Chef includes testing tools that support this approach. As Figure 16-4 shows, Chef provides multiple testing tools specialized to give you feedback on issues with your code at the earliest possible time during the cookbook authoring process.
Chef’s testing tools for every phase of development
Figure 16-4. Chef’s testing tools for every phase of development
There are multiple tools because each is tailored to run in a particular cookbook authoring phase. Following is a brief overview of each tool and when you use it:
  • In your text editor when you type:
    • Foodcritic analyzes your Chef coding style.
  • Before you deploy to a test node:
    • ChefSpec helps you document and organize your code.
  • After you deploy to a test node:
    • Serverspec verifies that a cookbook behaves as intended.

NOTE

In this chapter, we use the terms test and example interchangeably.
For those using Chef Client, you will need to install some additional gems to support testing. Run the following to install the required tools for this chapter:
$ sudo gem install foodcritic --no-ri --no-rdoc
$ sudo gem install chefspec --no-ri --no-rdoc
If you’re using the Chef Development Kit, you’re fine, these Ruby gems have already been installed for you.

Revisiting the Apache Cookbook

For people new to automated testing, Serverspec is the most easily understood tool, so we’ll start with it first. We will test it by revisiting a cookbook we created in Chapter 7. We’ll be adding tests to this cookbook in this chapter.

NOTE

We are covering Serverspec v2 syntax.
Generate a cookbook called apache-test.
Chef Development Kit:
$ chef generate cookbook apache-test
$ cd apache-test
Chef Client:
$ knife cookbook create apache-test --cookbook-path .
$ cd apache-test
$ kitchen init --create-gemfile
$ bundle install
Edit the .kitchen.yml as shown in Example 16-1. Use the chef_zero provisioner and our favorite basebox image. Additionally, configure a private_network with an IP address of 192.168.33.38 so you can access the website from your host development workstation like you did in Chapter 7.
Example 16-1. chefdk/apache-test/.kitchen.yml
---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: centos65
    driver:
      box: learningchef/centos65
      box_url: learningchef/centos65
      network:
      - ["private_network", {ip: "192.168.33.38"}]

suites:
  - name: default
    run_list:
      - recipe[apache-test::default]
    attributes:
Make sure there are no syntax errors in your .kitchen.yml by running kitchen converge:
$ kitchen converge
Create a default recipe with the same code we used in Chapter 7.
Example 16-2. chefdk/apache-test/recipes/default.rb
#
# Cookbook Name:: apache-test
# Recipe:: default
#
# Copyright (C) 2014
#
#
#

package "httpd"

service "httpd" do
  action [ :enable, :start ]
end

template "/var/www/html/index.html" do
  source 'index.html.erb'
  mode '0644'
end
Create an ERB template for the index.html file.
Chef Development Kit:
$ chef generate template index.html
Chef Client:
$ touch templates/default/index.html.erb
Let’s do a little something new with the index.html.erb template. In Introducing the Template Resource we learned that the ERB template processor looks for tags such as <%= %> in .erb files, evaluates the expression within the tag, and returns a string as output:
This site was set up by <%= node['hostname'] %>
If the tag does not have an equal sign, it is evaluated as a scriptlet instead of a string. Within Chef templates, this mechanism is used to add conditional logic. For example, take a close look at this ERB template:
<% for @interface in node['network']['interfaces'].keys %>
  * <%= @interface %>
<% end %>

TIP

Pay close attention to where <% %> is used and where <%= %> is used.
On my test node, which has three network interfaces—loeth0, and eth1 —the scriptlet will render the following output:
* lo
* eth0
* eth1
The lines in the ERB template without an equal sign <% %> are evaluated as scriptlets; the expression is evaluated but not rendered in the output file as a string. Then we use the ERB tag with an equals sign <%= %> to print out a line in the resulting output each time we run through the conditional logic.

NOTE

In order to determine that node['network']['interfaces'] was the correct variable syntax, we inspected the output of ohai, looking for the values that we wanted to display.
In your cookbook, edit index.html.erb as shown in Example 16-3. We’ll use a variant of the preceding logic that prints out each interface name on the node and its IP address, using more idiomatic Ruby and some bare-bones HTML.
Example 16-3. chefdk/apache-test/templates/default/index.hmtl.erb
<html>
<body>
<pre><code>
This site was set up by <%= node['hostname'] %>
My network addresses are:
<% node['network']['interfaces'].keys.each do |iface_name| %>
  * <%= iface_name %>:
      <%= node['network']['interfaces'][iface_name]['addresses'].keys[1] %>
<% end %>
</code></pre>
</body>
</html>

NOTE

This is admittedly horrible HTML! But it will render readable output, which is our only goal. Thankfully, this is a book on Chef, not HTML.
Perform a final kitchen converge to ensure there are no syntax errors in your code:
$ kitchen converge
Verify that the website functions as intended by visiting it at 192.168.33.38. It should render as shown in Figure 16-5
Your apache site on 192.168.33.38
Figure 16-5. Your apache site on 192.168.33.38

Test Automation with Serverspec

In the last section, we verified that our cookbook worked by running kitchen converge and verifying that the website worked by checking it manually. Let’s automate our website verification process by writing some tests in Serverspec and running the tests with Test Kitchen.

WRITE YOUR FIRST SERVERSPEC TEST

By default, Test kitchen will look in the test/integration subdirectory for test-related files. Serverspec looks for its own files a few more directory levels below test/integration. First there needs to be a directory name underneath test/integration that matches the suite name:
<cookbook_root>
└── test
    └── integration
        └── <suite_name>
As Figure 16-6 shows, the name of the suite can be found in the suites: stanza of the .kitchen.ymldefault is the suite name that is initially generated. Although not covered in this book, you can use the suite capability of Test Kitchen to run sets of tests with different run lists and attributes, perhaps exercising conditional functionality in your cookbook.
Finding the suite name in the .kitchen.yml
Figure 16-6. Finding the suite name in the .kitchen.yml
In our case, the suite name is default, so create a directory for that now under the apache-test cookbook root.
Linux/Mac OS X:
$ mkdir -p test/integration/default
Windows:
> mkdir test\integration\default
Further, we need to create a directory underneath test/integration/default to tell test kitchen that we want to use the serverspec test plugin. Rather than specifying this in the .kitchen.yml file, Test Kitchen infers this from the directory structure underneath test/integration. Create the test/integration/default/serverspec directory now.
Linux/Mac OS X:
$ mkdir -p test/integration/default/serverspec
Windows:
$ mkdir test\integration\default\serverspec
By convention, Serverspec expects files containing test code to end in the suffix spec.rb. Create the file default_spec.rb in the test/integration/default/serverspec subdirectory of your cookbook, as shown in Example 16-4.
Example 16-4. chefdk/apache-test/test/integration/default/serverspec/default_spec.rb
require 'serverspec'

set :backend, :exec

describe 'web site' do
  it 'responds on port 80' do
    expect(port 80).to be_listening 'tcp'
  end
end
There are three major components to the spec.rb file, which we’ve labeled 1, 2, and 3:
require 'serverspec' 1

set :backend, :exec 2

describe 'web site' do 3
  it 'responds on port 80' do
    expect(port 80).to be_listening 'tcp'
  end
end
1
The require statement is used to load the gem library for serverspec, so we can reference Serverspec classes and methods, such as the set method.
2
The set statement lets us configure how serverspec runs. In this case, we set the :backend property to :exec to tell serverspec that the test code will be running on the same machine as where it is being evaluated.
3
Tests are written using the RSpec DSL using describe and it statements. In this case, we’re using the RSpec DSL to write a test that checks to see if our website is listening on port 80 using the TCP protocol (the standard port/protocol for an HTTP website).
We’ll go over the RSpec DSL and test syntax in more detail in RSpec DSL Syntax. For now, just accept that this is the syntax to specify a test that checks to see if something is listening on port 80.
In order to run this test code, you first need to make sure all the necessary gem files are loaded on the test node. We do that with the kitchen setup command. Go ahead and run kitchen setup now:
$ kitchen setup
-----> Starting Kitchen (v1.2.1)
-----> Setting up <default-centos65>...
Fetching: thor-0.19.0.gem (100%)
Fetching: busser-0.6.2.gem (100%)
       Successfully installed thor-0.19.0
       Successfully installed busser-0.6.2
       2 gems installed
-----> Setting up Busser
       Creating BUSSER_ROOT in /tmp/busser
       Creating busser binstub
       Plugin serverspec installed (version 0.4.0)
-----> Running postinstall for serverspec plugin
       Finished setting up <default-centos65> (0m14.02s).
-----> Kitchen is finished. (0m14.64s)
When the kitchen setup command runs, it inspects the directory structure in the test/integration/<suite>/<plugin> subfolder on your development host. Test Kitchen will then load any plugins required for testing in your sandbox instance as indicated by the <plugin> directory name. Because we created the directory subfolder test/integration/default/serverspec, Test Kitchen makes sure that the test node has all the necessary libraries and gems for running serverspec tests.
Once the test node has all the appropriate test libraries, you can run your tests using the kitchen verifycommand. Do that now. Barring any errors in your code syntax, the output should successfully indicate that the website is running and responding on port 80:
$ kitchen verify
-----> Starting Kitchen (v1.2.1)
-----> Verifying <default-centos65>...
       Suite path directory /tmp/busser/suites does not exist, skipping.
       Uploading /tmp/busser/suites/serverspec/default_spec.rb (mode=0644)
-----> Running serverspec test suite
       /opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec
       -I/tmp/busser/gems/gems/rspec-support-3.1.2/lib:/tmp/busser/gems/gems/
       rspec-core-3.1.7/lib /opt/chef/embedded/bin/rspec --pattern /tmp/busser
       /suites/serverspec/\*\*/\*_spec.rb --color --format documentation
       --default-path /tmp/busser/suites/serverspec

       web site
         responds on port 80

       Finished in 0.04131 seconds (files took 0.20083 seconds to load)
       1 example, 0 failures
       Finished verifying <default-centos65> (0m0.90s).
-----> Kitchen is finished. (0m1.36s)
You can use these RSpec DSL test statements to express checks in code that you would do manually to verify that your cookbook is working.
However, did this test really do anything? Let’s verify this by temporarily disabling the website on the test node and running the test code again.
Log in to the test node with kitchen login, stop the web server with service httpd stop, and exit back out to your host development workstation command prompt, as shown in the following code block:
$ kitchen login
Last login: Mon Aug 11 21:01:36 2014 from 10.0.2.2
Welcome to your Packer-built virtual machine.
[vagrant@default-centos65 ~]$ sudo service httpd stop
Stopping httpd:                                            [  OK  ]
[vagrant@default-centos65 ~]$ exit
logout
Connection to 127.0.0.1 closed.
Run your test again using kitchen verify (you only need to run kitchen setup once to initialize the configuration on the test node). This time, it should report that the test failed, which is exactly what should happen:
$ kitchen verify
-----> Starting Kitchen (v1.2.1)
-----> Verifying <default-centos65>...
       Removing /tmp/busser/suites/serverspec
       Uploading /tmp/busser/suites/serverspec/default_spec.rb (mode=0644)
-----> Running serverspec test suite
       /opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec
       -I/tmp/busser/gems/gems/rspec-support-3.1.2/lib:/tmp/busser/gems/gems
       /rspec-core-3.1.7/lib /opt/chef/embedded/bin/rspec --pattern /tmp/busser
       /suites/serverspec/\*\*/\*_spec.rb --color --format documentation
       --default-path /tmp/busser/suites/serverspec

       web site
         responds on port 80 (FAILED - 1)

       Failures:

         1) web site responds on port 80
            Failure/Error: expect(port 80).to be_listening 'tcp'
              expected Port "80" to be listening "tcp"
              /bin/sh -c netstat\ -tunl\ \|\ grep\ --\ :80

            # /tmp/busser/suites/serverspec/default_spec.rb:7:in `block (2
            levels) in <top (required)>'

       Finished in 0.03284 seconds (files took 0.2051 seconds to load)
       1 example, 1 failure
...
So yes, our test is actually performing checks against the live configuration on the test node.
Before we finish this section, go ahead and run kitchen converge again to restore the configuration back to what it should be. During the Chef run, the Chef engine will detect that the httpd service is not running and start it up again:
$ kitchen converge
Once the web server is restored, run kitchen verify one final time. The test should pass once again:
$ kitchen verify
-----> Starting Kitchen (v1.2.1)
-----> Setting up <default-centos65>...
-----> Setting up Busser
       Creating BUSSER_ROOT in /tmp/busser
       Creating busser binstub
       Plugin serverspec already installed
       Finished setting up <default-centos65> (0m1.06s).
-----> Verifying <default-centos65>...
       Removing /tmp/busser/suites/serverspec
       Uploading /tmp/busser/suites/serverspec/default_spec.rb (mode=0644)
-----> Running serverspec test suite
       /opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec
       -I/tmp/busser/gems/gems/rspec-support-3.1.2/lib:/tmp/busser/gems/gems
       /rspec-core-3.1.7/lib /opt/chef/embedded/bin/rspec --pattern /tmp/busser
       /suites/serverspec/\*\*/\*_spec.rb --color --format documentation
       --default-path /tmp/busser/suites/serverspec

       web site
         responds on port 80

       Finished in 0.03131 seconds (files took 0.19949 seconds to load)
       1 example, 0 failures
       Finished verifying <default-centos65> (0m0.90s).
-----> Kitchen is finished. (0m2.66s)

RSPEC DSL SYNTAX

Before we continue learning more about how to use Serverspec, let’s go over some of the fundamentals of the RSpec DSL syntax, so you know the basics of the Serverspec test syntax.
The RSpec DSL uses a describe block to contain a group of tests. A describe block has the following form:
describe '<entity>' do
  <tests here>
end
The purpose of the describe block is to group tests in a meaningful manner and describe the entity or thing being tested. The description is just a string passed as a parameter to describe. This string serves as documentation for human beings to read in the test output. In Example 16-4, we used the following describeform to note that we are testing our website:
describe 'web site' do
  <tests here>
end

NOTE

Under the hood, the RSpec DSL creates a Ruby class in which to group tests.
The actual tests are contained within an it block inside a describe, which needs to be in the following form:
describe '<entity>' do
  it '<description>'
    <examples here>
  end
end
The it block also accepts a string for documentation on the specific check that will be performed. For example, in Example 16-4 we supplied the string responds on port 80 to indicate that our test will be checking to see if the website responds on port 80, the standard HTTP port:
describe 'web site' do
  it 'responds on port 80' do
    ...
  end
end
As of RSpec 3.0, the version of RSpec that ships with current versions of the Chef Development Kit and Chef Client, the tests themselves should be written in expect form. Here’s what expect form looks like:
describe '<entity>' do
  it '<description>'
    expect(resource).to matcher matcher_parameter
  end
end
resource (also known as a subject or command) is the first argument for an expect block, and it expresses the “thing” to be tested. Testing frameworks such as Serverspec and ChefSpec supply custom resource class implementations that perform a wide variety of checks.
matcher is used to define positive or negative expectations on a resource, via the expect(…).to and expect(…).not_to forms, respectively. These are also supplied as custom class implementations in testing frameworks.
In Example 16-4, we used the port resource and the be_listening matcher with the parameter tcp to check to see if the website is listening on port 80 over TCP:
describe 'web site' do
  it 'responsponds on port 80' do
    expect(port 80).to be_listening 'tcp'
  end
end
How did we know about this port resource and the be_listening matcher? We referred to the Serverspec test framework documentation listing the resource and matcher classes it provides. See the Serverspec documentation. As of this writing, click on the Resource Types link at the top of the page, and you will see links to all the Serverspec custom resources, as shown in Figure 16-7.
Serverspec resource documentation
Figure 16-7. Serverspec resource documentation
When you click on a resource, you’ll see more detail on all the matchers available, as shown in Figure 16-8.
Serverspec port resource be_listening matcher documentation
Figure 16-8. Serverspec port resource be_listening matcher documentation
As you can see from this documentation, you’ll also encounter a legacy RSpec form that was used prior to RSpec 3.0: the should form. The should form was deprecated with RSpec 3.0, because it can produce unexpected results for some testing code edge cases. However, some sites, such as the Serverspec documentation site, haven’t been updated.
Figure 16-9 shows how you can map old documentation in should form to expect form. With should form, the resource is in a describe block around the it clause. With expect form, this is a parameter passed to expect. You can also see how the matcher form differs. With should, expectations are expressed as should or should not, for positive and negative expectations, respectively. With expect, expectations are expressed as the chained methods .to or .not_to. Finally, in should form, matcher parameters are expessed using a chained .with()syntax, whereas in expect form, it is just a parameter to the matcher.
Expect versus should form
Figure 16-9. Expect versus should form

MORE SERVERSPEC RESOURCES

Common code can be moved to a file called spec_helper.rb. We only have one file with tests in our example, but imagine there are multiple files. Create a spec_helper.rb as shown in Example 16-5. Notice that the file contains the first two lines from default_spec.rb. Those lines would need to be repeated in every file that contains tests.
Example 16-5. chefdk/apache-test/test/integration/default/serverspec/spec_helper.rb
require 'serverspec'

set :backend, :exec
Now that you have a spec_helper.rb file, modify default_spec.rb to use the spec_helper. Change the requirestatement and remove the set line, as shown in Example 16-6.
Example 16-6. chefdk/apache-test/test/integration/default/serverspec/default_spec.rb
require 'spec_helper'

describe 'web site' do
  it 'responds on port 80' do
    expect(port 80).to be_listening 'tcp'
  end
end
Rerun kitchen verify. You should notice no net change in the tests. It should still report that one example succeeded.
$ kitchen verify
Although it is a little silly to use a spec_helper.rb file in this contrived example, we hope you see how this file could be used to contain any duplicate code between multiple files with tests.
You can add more than one example with tests in a describe block. Normally, there will a handful to perhaps a dozen. Let’s add one more example to default_spec.rb.
Although we’ve written one example that checks to see that our website is responding on port 80, we don’t really know if it is serving up the correct content. Let’s write an example that inspects the website output to see if it seems OK.
If you look at the Serverspec documentation, you’ll find that there isn’t an obvious resource that seems to do what we want. In cases like this, Serverspec lets you run arbitrary command lines via the command resource as shown in Figure 16-10. We’ll use the command resource to run a curl command to inspect the website output, just as we did in Chapter 7.
Command resource
Figure 16-10. Command resource

NOTE

The its method is a way to access attributes of a resource in should form. To access resource attributes in expect form, use a chained method with the attribute name, using statements like:
expect(command(...).attribute)
Add a new example to default_spec.rb as shown in Example 16-7. By running the curl localhost command, you can inspect the HTML output of the website to ensure that it is working correctly.
Using the stdout attribute of the command resource, we can take a look at the output of the curl command returned on standard output. It is a convention that programs generate their output to two different standard file handles: stdout and stderr, for regular program output and errors, respectively. This way, other computer programs can open these file handles and inspect the contents in an automated fashion. Our example does not care about any errors happening on stderr because getting notified that an error happened through Ruby exceptions is enough for test code. This error exception generation process happens automatically in Serverspec. We only care about the program output being generated on stdout.
The results of running curl localhost:80 are returned to our example code as a string. We use a feature of Ruby called a regular expression and the match RSpec matcher to search for content in the output generated by curl. In Ruby, strings containing regular expressions are enclosed by forward slash characters (//) instead of the usual single quotes ('') or double quotes (“”).
A regular expression is a special string format that is used to specify a search string. In this case, we use a regular expression to search for the string eth1 in the program output. This seems like a reasonable and simple way to check that our website is working. It isn’t likely that the string eth1 would appear in the output otherwise. Using the string eth1 also implicitly checks to make sure that the vagrant box had its eth1 adapter enabled, which is another assumption we’d like to check as well. When there are opportunities to implicitly check more than one condition in your tests, take the opportunity to do so.
Example 16-7. chefdk/apache-test/test/integration/default/serverspec/default_spec.rb
require 'spec_helper'

describe 'web site' do
  it 'responds on port 80' do
    expect(port 80).to be_listening 'tcp'
  end

  it 'returns eth1 in the HTML body' do
    expect(command('curl localhost:80').stdout).to match /eth1/
  end
end
There’s a great website for learning more about regular expressions in Ruby. You can use this website to check regular expressions against test strings. Let’s use it to check our regular expression.
First, log in to the node with kitchen login and run the same curl localhost:80 command that we will run in our test. The output is shown in the following example. Copy and paste the program output to your clipboard from your terminal window. Then exit back out to your host prompt:
$ kitchen login
Last login: Sun Aug 17 10:39:34 2014 from 10.0.2.2
Welcome to your Packer-built virtual machine.
[vagrant@default-centos65 ~]$ curl localhost:80
<html>
<body>
<pre><code>
This site was set up by default-centos65
My network addresses are:
  * lo: ::1
  * eth0: 10.0.2.15
  * eth1: 192.168.33.38
</code></pre>
</body>
</html>
[vagrant@default-centos65 ~]$ exit
logout
Connection to 127.0.0.1 closed.
Once you have the program output in your host clipboard, paste it into the test string field as shown in Figure 16-11.
Once you have a test string, you can enter in any regular expression between the forward slash characters in the Your regular expression field. Once you enter a regular expression, the Rubular site will show you if it would match any results in the test string.
Regular expressions containing strings without any special symbols will match the string itself. For more information about the special characters that can be used, refer to the Regex quick reference section of the Rubular website, below the editing pane. As Figure 16-12 shows, if we use the eth1 regular expression and the program output contains eth1, we’ll get a match. And conversely, if the program output does not contain eth1, we won’t get a match. Thus, we make a match if the regular expression eth1 against the program output of curl localhost:80 on stdout is a success condition for our example.
Copy and paste a test string on Rubular
Figure 16-11. Copy and paste a test string on Rubular
Run kitchen verify. Notice from the Serverspec report that the second example succeeds. Serverspec ran curl localhost:80 on the node and got the expected regular expression match in the program output:
$ kitchen verify
-----> Starting Kitchen (v1.2.1)
...
-----> Running serverspec test suite
       /opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec
       -I/tmp/busser/gems/gems/rspec-support-3.1.2/lib:/tmp/busser/gems/gems
       /rspec-core-3.1.7/lib /opt/chef/embedded/bin/rspec --pattern /tmp/busser
       /suites/serverspec/\*\*/\*_spec.rb --color --format documentation
       --default-path /tmp/busser/suites/serverspec

       web site
         responds on port 80
         returns eth1 in the HTML body

       Finished in 0.03838 seconds (files took 0.20863 seconds to load)
       2 examples, 0 failures
       Finished verifying <default-centos65> (0m1.25s).
-----> Kitchen is finished. (0m1.71s)
Inspect match results
Figure 16-12. Inspect match results
Just from these two simple examples, we can be fairly certain about whether the website our Chef code produces actually works. Further, when an error occurs, we can more easily determine whether it was a fault in the Apache webserver setup or in our HTML code because we check these two conditions separately.
There’s one more thing we need to cover before you can go about using all of the Serverspec resources in your own test code. Many Serverspec resources require Serverspec to detect information about the test node operating system so it can run the correct commands for the platform. We’ve taken care so far not to use any commands that need this extra support.
The package resource is a command that requires Serverspec to detect OS information. Figure 16-13 shows the documentation from the Serverspec site on the service resource. It can be used to detect whether a package is installed. Behind the scenes, it needs to know what OS is being used so it can use the rpm -q command on RedHat and variants or dpkg-query on Ubuntu/Debian to perform this query, for example.
Serverspec package resource
Figure 16-13. Serverspec package resource
By default, Serverspec tries to automatically detect the OS, which works for most Linux/Unix variants. However, on some platforms you’ll need to override the default OS setting, using the set method.
In particular, you’ll need to add the following line to all your _spec.rb files for test code that you plan to run on Windows guests, as Serverspec is unable to automatically detect the Windows OS, as of this writing. The exact set commands needed vary by platform. Refer to the Serverspec documentation for more information.
Here’s what a _spec.rb file might look like for Windows, which uses the set command to give Serverspec a cue on what OS is running:
require 'spec_helper'
set :backend, :cmd
set :os, :family => 'windows'
In this book, we’re using a Linux variant as our guest OS, so the Serverspec autodetect logic should work just fine. Make sure you check this out on your test platform by using a resource that requires OS platform detection, like the package command.
Let’s add an example to default_spec.rb using the package command. As shown in Example 16-8, let’s check to see if the httpd package is installed.
Example 16-8. chefdk/apache-test/test/integration/default/serverspec/default_spec.rb
require 'spec_helper'

describe 'web site' do
  it 'responds on port 80' do
    expect(port 80).to be_listening 'tcp'
  end

  it 'returns eth1 in the HTML body' do
    expect(command('curl localhost:80').stdout).to match /eth1/
  end

  it 'has the apache web server installed' do
    expect(package 'httpd').to be_installed
  end
end
Run kitchen verify for Serverspec. The command works fine because Serverspec automatically detected the OS:
$ kitchen verify
-----> Starting Kitchen (v1.2.1)
...
-----> Running serverspec test suite
       /opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec
       -I/tmp/busser/gems/gems/rspec-support-3.1.2/lib:/tmp/busser/gems/gems
       /rspec-core-3.1.7/lib /opt/chef/embedded/bin/rspec --pattern /tmp/busser
       /suites/serverspec/\*\*/\*_spec.rb --color --format documentation
       --default-path /tmp/busser/suites/serverspec

       web site
         responds on port 80
         returns eth1 in the HTML body
         has apache installed

       Finished in 0.03944 seconds
       3 examples, 0 failures
       Finished verifying <default-centos65> (0m1.25s).
-----> Kitchen is finished. (0m1.74s)
If for some reason we need to tell Serverspec that we are specifically running CentOS 6 because of issues with a command, we can add the following set line as shown in Example 16-9:
set :os, :family => 'redhat', :release => 6
CentOS is in the RedHat family of operating systems. Specifying a :release attribute is optional.
Example 16-9. chefdk/apache-test/test/integration/default/serverspec/default_spec.rb
require 'spec_helper'

set :os, :family => 'redhat', :release => 6

describe 'web site' do
  it 'responds on port 80' do
    expect(port 80).to be_listening 'tcp'
  end

  it 'returns eth1 in the HTML body' do
    expect(command('curl localhost:80').stdout).to match /eth1/
  end

  it 'has the apache web server installed' do
    expect(package 'httpd').to be_installed
  end
end
When you are not interactively coding tests, you probably want to run kitchen convergekitchen setup, and so on all in one fell swoop instead of needing to remember all the individual Test Kitchen actions to run tests.
The kitchen test command will run the following commands in sequence:
  1. kitchen destroy (if necessary)
  2. kitchen create
  3. kitchen converge
  4. kitchen setup
  5. kitchen verify
  6. kitchen destroy
You wouldn’t want to use this command locally when you are writing tests, as for some cookbooks the process of creating a virtual machine and performing an initial converge can be quite time consuming, and you wouldn’t want the environment automatically destroyed in the end. But Test Kitchen is a perfect command for a continual integration environment such as Jenkins. It’s also a good idea to do a final kitchen test run against a clean setup before committing your Chef code to source control.
Go ahead and run kitchen test now, so you can see it in action. Plus, it will destroy our test environment. Note that kitchen test runs all five phases automatically for you:
$ kitchen test
-----> Starting Kitchen (v1.2.1)
-----> Cleaning up any prior instances of <default-centos65>
-----> Destroying <default-centos65>...
...
-----> Testing <default-centos65>
-----> Creating <default-centos65>...
       Bringing machine 'default' up with 'virtualbox' provider...
...
       Vagrant instance <default-centos65> created.
       Finished creating <default-centos65> (0m35.57s).
-----> Converging <default-centos65>...
       Preparing files for transfer
       Resolving cookbook dependencies with Berkshelf 3.1.5...
       Removing non-cookbook files before transfer
-----> Installing Chef Omnibus (true)
...
       Thank you for installing Chef!
       Transferring files to <default-centos65>
       [2014-08-11T18:33:39-07:00] INFO: Starting chef-zero on host localhost,
       port 8889 with repository at repository at /tmp/kitchen
         One version per cookbook

       [2014-08-11T18:33:39-07:00] INFO: Forking chef instance to converge...
       Starting Chef Client, version 11.14.2
       [2014-08-11T18:33:39-07:00] INFO: *** Chef 11.14.2 ***
...
       [2014-08-11T18:33:42-07:00] INFO: Starting Chef Run for default-centos65
...
       Converging 3 resources
...
       [2014-08-11T18:33:56-07:00] INFO: Report handlers complete
       Chef Client finished, 4/4 resources updated in 17.027821539 seconds
       Finished converging <default-centos65> (1m11.46s).
-----> Setting up <default-centos65>...
Fetching: thor-0.19.0.gem (100%)
Fetching: busser-0.6.2.gem (100%)
       Successfully installed thor-0.19.0
       Successfully installed busser-0.6.2
       2 gems installed
-----> Setting up Busser
       Creating BUSSER_ROOT in /tmp/busser
       Creating busser binstub
       Plugin serverspec installed (version 0.2.6)
-----> Running postinstall for serverspec plugin
       Finished setting up <default-centos65> (0m24.61s).
-----> Verifying <default-centos65>...
       Suite path directory /tmp/busser/suites does not exist, skipping.
       Uploading /tmp/busser/suites/serverspec/default_spec.rb (mode=0644)
       Uploading /tmp/busser/suites/serverspec/spec_helper.rb (mode=0644)
-----> Running serverspec test suite
       /opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec
       -I/tmp/busser/gems/gems/rspec-support-3.1.2/lib:/tmp/busser/gems/gems
       /rspec-core-3.1.7/lib /opt/chef/embedded/bin/rspec --pattern /tmp/
       busser/suites/serverspec/\*\*/\*_spec.rb --color
       --format documentation --default-path /tmp/busser/suites/serverspec

       web site
         responds on port 80
         returns eth1 in the HTML body
         has the apache web server installed

       Finished in 0.03922 seconds
       3 examples, 0 failures
       Finished verifying <default-centos65> (0m1.14s).
-----> Destroying <default-centos65>...
       ==> default: Forcing shutdown of VM...
       ==> default: Destroying VM and associated drives...
       Vagrant instance <default-centos65> destroyed.
       Finished destroying <default-centos65> (0m2.89s).
       Finished testing <default-centos65> (2m19.03s).
-----> Kitchen is finished. (2m19.48s)
For more on Serverspec, the Jenkins community cookbook is chock-full of advanced Serverspec techniques. It is a great starting point to learn more about how to perform end-to-end testing of cookbooks.

Test Automation with Foodcritic

Severspec is an invaluable tool for performing end-to-end testing of cookbook functionality. However, spinning up a sandbox instance and performing a full Chef converge can take a long time.
Use the power of Test Kitchen and Serverspec judiciously. Other tools can provide more limited forms of feedback faster. One example of a tool that can provide limited feedback quickly is Foodcritic.
Foodcritic is designed to be used as you are writing Chef code, and it can even be integrated into your editor. Foodcritic provides feedback on your Chef coding style. It does this by performing checks against your code called rules.
You can find all the default rules used by Foodcritic in its documentation, as shown in Figure 16-14. You’ll need to scroll down a bit on the web page to see them.
You run foodcritic on your development host instead of in a sandbox environment, so it is fast. Give it a try now. Make sure the root apache-test root cookbook is your current working directory, and run the following. The results you see might differ depending on whether you are using the Chef Development Kit or Chef Client.
Chef Development Kit:
$ foodcritic .

Chef Client:
$ foodcritic .
FC008: Generated cookbook metadata needs updating: ./metadata.rb:2
FC008: Generated cookbook metadata needs updating: ./metadata.rb:3
Foodcritic rules
Figure 16-14. Foodcritic rules
As of this writing, there is a bug in the version of Foodcritic shipping with Chef Development Kit 0.2.0-2. It should check to see whether the metadata.rb file needs updating, as shown in Figure 16-15, but it doesn’t currently work with the cookbook output generated by chef cookbook generate.
We’re not sure if this rule will be fixed in future versions, if a new rule will be added to check for the Chef Development Kit version, or something else. So for now, if you are using the Chef Development Kit, change your metadata.rb file to match the Chef Client-generated version, as shown in Example 16-10.
Example 16-10. chefdk/apache-test/metadata.rb
name             'apache-test'
maintainer       'YOUR_COMPANY_NAME'
maintainer_email 'YOUR_EMAIL'
license          ''
description      'Installs/Configures apache-test'
long_description 'Installs/Configures apache-test'
version          '0.1.0'
Foodcritic in action, telling us that some cookbook metadata needs updating
Figure 16-15. Foodcritic in action, telling us that some cookbook metadata needs updating
After this change, the Chef Development Kit result should match the Chef Client version.
Chef Development Kit:
$ foodcritic .
FC008: Generated cookbook metadata needs updating: ./metadata.rb:2
FC008: Generated cookbook metadata needs updating: ./metadata.rb:3
As shown in Figure 16-15, when Foodcritic detects an issue in your Chef code, you can look up more detail on the issue and how it can be fixed. In this case, FC008 indicates that you should modify the metadata.rb file maintainer and maintainer_email fields to be something besides the default boilerplate text.
Let’s modify the metadata.rb file appropriately. Example 16-11 shows how we changed our file.
Example 16-11. chefdk/apache-test/metadata.rb
name             'apache-test'
maintainer       'Mischa Taylor'
maintainer_email 'mischa@misheska.com'
license          'MIT'
description      'Installs/Configures apache-test'
long_description 'Installs/Configures apache-test'
version          '0.1.0'
Run Foodcritic again. It should now report that FC008 is no longer an issue:
$ foodcritic .

Let’s create one more issue with our code. We’ll say that a Chef developer forgot to commit a README.md file into source control. Simulate this state by renaming the README.md file. Run the following move command:
$ mv README.md README.md.old
Run Foodcritic as follows and you should see a new issue:
$ foodcritic .
FC011: Missing README in markdown format: ./README.md:1
As you can see from the documentation on FC11, as shown in Figure 16-16, it is important to provide a README file in markdown format because Chef Supermarket expects your cookbooks to have documentation in a README.md file.
FC011: Missing README in markdown
Figure 16-16. FC011: Missing README in markdown
Ideally, you would fix this issue by writing some great documentation, but for now, just move the README.mdboilerplate back to its original name:
$ mv README.md.old README.md
Run Foodcritic again, and it should report that FC011 is no longer an issue:
$ foodcritic .

Performing these Foodcritic checks should be a regular part of your Chef cookbook development cycle. TheFoodcritic documentation has more information on how to integrate Foodcritic with various build tools.
You can even use Foodcritic with many popular text editors, so it can perform Foodcritic runs while you type or when you save your Chef code.
You can create your own custom rules to extend the checks performed by Foodcritic. Etsy has published its set of custom Foodcritic rules online. You can use its custom rules as an example of how you can write rules more relevant to your environment.
Not all Foodcritic rules are trivial checks to see if you filled in the metadata.rb file or provided documentation. The Etsy custom Foodcritic rules, for example, check for issues that have caused outages in their production environment, such as ETSY001, as shown in Figure 16-17.
ETSY001 - Package or yum_package resource used with :upgrade action
Figure 16-17. ETSY001 - Package or yum_package resource used with :upgrade action
Foodcritic can be used to perform vital checks similar to ETSY001, to catch bugs in your code even before it gets deployed to a testing sandbox environment. A way to start might be to look through recent help desk incidents where you have identified the root cause being related to server configuration issues, and encode them as Foodcritic rules. This is the process Etsy used to develop its custom Foodcritic rule set.
We hope this section on Foodcritic shows you how you can catch bugs earlier in your Chef development process, closer to the time of coding. Catching issues early saves time and money.
Although limited in the feedback it can provide, Foodcritic is a great tool for catching many bugs early. You still need to do some form of end-to-end testing using a tool like Serverspec, but you shouldn’t have to rely on end-to-end testing exclusively to find issues.

Test Automation with ChefSpec

Another great tool that can help you run tests early in your development cycle is ChefSpec. You can even use it to catch errors before you code. ChefSpec can be used to produce runnable documentation. Its primary purpose is to help document and organize your code.
As a side benefit, ChefSpec tests and checks can uncover bugs when you make changes. Plus, your Chef code will be improved when it is guided by tests.
Similar to Serverspec, ChefSpec builds on RSpec. ChefSpec uses the RSpec description form to create runnable documentation. The form for ChefSpec documentation is slightly different from Serverspec’s, resembling the following:
describe '<recipe_name>' do
  <perform in-memory Chef run>
  <examples here>
end
For example, you would use the following describe block to contain examples performing tests against the apache-test::default cookbook:
describe 'apache-test::default' do
  ...
end
To perform an in-memory Chef run, you would add the following statements to the basic describe form, using classes and methods from the chefspec gem. In this example, to test the apache-test::default cookbook, you would use the following code:
require 'chefspec'

describe 'apache::default' do
  chef_run = ChefSpec::Runner.new.converge('apache-test::default')
  <descriptions here>
end
ChefSpec uses an expect form similar to Serverspec’s. There are just different commands and matchers for ChefSpec. Following is a ChefSpec example that checks to make sure there is a reference in your Chef code to install the httpd package:
require 'chefspec'

describe 'apache::default' do
  chef_run = ChefSpec::Runner.new.converge('apache-test::default')

  it 'installs apache2' do
    expect(chef_run).to install_package('httpd')
  end
end
Keep in mind that the preceding code is just runnable documentation. The expect statement does not perform an httpd package installation during the in-memory Chef run. Instead, ChefSpec merely performs the in-memory Chef run to verify the cookbook syntax; in this case, to ensure that your code instructed Chef to install the package. This form of documentation-based testing is good enough for well-tested Chef primitives, such as the package resource.
Commands in ChefSpec are usually the results of an in-memory Chef run. ChefSpec matchers are documented[ChefSpec matchers] are documented as shown in Figure 16-18.
ChefSpec documentation
Figure 16-18. ChefSpec documentation
If you expand the ChefSpec tree in the index on the left, you’ll see all the ChefSpec matchers listed, plus detailed examples, as shown in Figure 16-19.
Each ChefSpec matcher has detailed examples
Figure 16-19. Each ChefSpec matcher has detailed examples

WRITE YOUR FIRST CHEFSPEC TEST

Let’s write some ChefSpec code. We’ll use the install_package matcher, as shown in Figure 16-20.
The default location for ChefSpec tests are in a spec folder underneath your cookbook root. Make sure that the apache-test root cookbook directory is the current working directory, and create a spec directory as follows:
$ mkdir spec
install_package matcher
Figure 16-20. install_package matcher
Create a file called default_spec.rb with the content shown in Example 16-12. Files containing ChefSpec code by convention are expected to end in the suffix *_spec.rb.
Example 16-12. chefdk/apache-test/spec/default_spec.rb
require 'chefspec'

describe 'apache-test::default' do
  chef_run = ChefSpec::Runner.new.converge('apache-test::default')

  it 'installs apache2' do
    expect(chef_run).to install_package('httpd')
  end
end
ChefSpec does not require any special chefspec command to run as it just extends RSpec. In the apache-testcookbook root, run rspec --color as shown in the following example to perform a ChefSpec run:
$ rspec --color
.

Finished in 0.00042 seconds (files took 1.12 seconds to load)
1 example, 0 failures
When rspec runs, ChefSpec will inspect your Chef code and make sure it uses the package resource to install the httpd package. If ChefSpec verifies that your code does this, the test passes, as in the rspec command you just ran.

LAZY EVALUATION WITH LET

We need to introduce one more bit of RSpec syntax: lazy evaluation using the let helper method, which is part of the RSpec core. Figure 16-21 shows how ChefSpec commonly uses let helper method to cache the results of the ChefSpec::Runner object.
Using ChefSpec with the let helper method
Figure 16-21. Using ChefSpec with the let helper method
A call to the ChefSpec::Runner is fairly heavyweight. A call to let() delays the evaluation of the ChefSpec::Runner until when it is first used instead of when it is referenced in the source—thus the evaluation is “lazy.” Using let() allows RSpec to cache the results of ChefSpec::Runner when it is used multiple times in the same example.
Further, the let() call permits you to specify the recipe under test only once in your describe block as documentation. Compare the following Without let and With let code examples, and notice that the call to ChefSpec::Runner uses a described_recipe macro to evaluate the recipe name instead of repeating the recipe string. A small optimization, but a useful one.
Without let:
require 'chefspec'

describe 'apache::default' do
  chef_run = ChefSpec::Runner.new.converge('apache-test::default')

  it 'installs apache2' do
    expect(chef_run).to install_package('httpd')
  end
end
With let:
require 'chefspec'

describe 'apache::default' do
  let (:chef_run) { ChefSpec::Runner.new.converge(described_recipe) }

  it 'installs apache2' do
    expect(chef_run).to install_package('httpd')
  end
end
Change the source in default_spec.rb to use the let() helper method as shown in Example 16-13.
Example 16-13. chefdk/apache-test/spec/default_spec.rb
require 'chefspec'

describe 'apache-test::default' do
  let (:chef_run) { ChefSpec::Runner.new.converge(described_recipe) }

  it 'installs apache2' do
    expect(chef_run).to install_package('httpd')
  end
end
When you rerun rspec, you should notice no net change in the test results:
$ rspec --color
.

Finished in 0.00042 seconds (files took 1.12 seconds to load)
1 example, 0 failures

GENERATE A COVERAGE REPORT

Another ChefSpec helper method is ChefSpec::Coverage.report!. It will generate a list of resources that have corresponding examples as documentation. You can let this report guide your testing.
Edit default_spec.rb as shown in Example 16-14. The at_exit method is a part of core Ruby that permits you to register a block to execute when the program exits. In this case, we want to run the ChefSpec::Coverage.report! method. The exclamation point (!) in the report! method name is a Ruby convention that indicates a method is dangerous. In this case, the cautions are thatChefSpec::Coverage.report! must be run after all tests are complete and not run more than once in a program. We use at_exit to ensure that report! is run once after the tests have finished.
Example 16-14. chefdk/apache-test/spec/default_spec.rb
require 'chefspec'

at_exit { ChefSpec::Coverage.report! }

describe 'apache-test::default' do
  let (:chef_run) { ChefSpec::Runner.new.converge(described_recipe) }

  it 'installs apache2' do
    expect(chef_run).to install_package('httpd')
  end
end
Run rspec --color with the new at_exit code, and notice that now a helpful report is generated, telling you the total number of resources in your code and how many have been tested in your specs:
$ rspec --color
.

Finished in 0.00337 seconds (files took 1.11 seconds to load)
1 example, 0 failures

ChefSpec Coverage report generated...

  Total Resources:   3
  Touched Resources: 1
  Touch Coverage:    33.33%

Untouched Resources:

  service[httpd]                     /recipes/default.rb:12
  template[/var/www/html/index.html]   /recipes/default.rb:16
Let this report guide you in choosing other tests to write for your code.

SHARE TEST CODE WITH SPEC_HELPER.RB

ChefSpec supports moving common code used in your tests to a spec_helper.rb file, similar to Serverspec.
As with Serverspec, you’ll have to imagine there are many test files in this example, and we’ll move the shared code to spec_helper.rb.
Create the file spec/spec_helper.rb with the content shown in Example 16-15. We are moving the require and at_exit calls to this shared file.
Example 16-15. chefdk/apache-test/spec/spec_helper.rb
require 'chefspec'

at_exit { ChefSpec::Coverage.report! }
Now edit spec/default_spec.rb as shown in Example 16-16 so it references spec_helper.
Example 16-16. chefdk/apache-test/spec/default_spec.rb
require 'spec_helper'

describe 'apache-test::default' do
  let (:chef_run) { ChefSpec::Runner.new.converge(described_recipe) }

  it 'installs apache2' do
    expect(chef_run).to install_package('httpd')
  end
end
When you run rspec, you should notice no net change in the program output from when you ran rspec in the last section, as all we did was move around some code:
$ rspec --color
.

Finished in 0.00337 seconds (files took 1.11 seconds to load)
1 example, 0 failures

ChefSpec Coverage report generated...

  Total Resources:   3
  Touched Resources: 1
  Touch Coverage:    33.33%

Untouched Resources:

  service[httpd]                     /recipes/default.rb:12
  template[/var/www/html/index.html]   /recipes/default.rb:16

Summary

In this chapter we discussed how to test your Chef automation using Serverspec, Foodcritic, and ChefSpec. You need multiple tools because each is tailored to give you fast feedback at every stage of the Chef development lifecycle.
To learn more about test automation with Chef, check out the slides for the one-day course written by one of the authors of this book. This chapter was based on these slides.

1 comment:

  1. Betway and Betway Casinos in Connecticut in 2021
    Betway has 양산 출장샵 become the 안양 출장마사지 best 양산 출장마사지 betting site 강릉 출장마사지 in the 김포 출장샵 U.S. by offering an impressive array of casino games and poker games to its customers.

    ReplyDelete