This blog post starts with a gist, and a tweet. However, that isn’t the whole story. Read on…

Today I released version 1.6.0 of Opscode’s apt cookbook. The cookbook itself needed better coverage for testing in Test Kitchen. This post will describe these additions to the cookbook, including how one of the test recipes can actually be used for actual production use. My goal is to explain a bit about how we go about testing with Test Kitchen, and provide some real world examples.

TL;DR - This commit has all the code.

Kitchenfile

First, the Kitchenfile for the project looked like this:

cookbook "apt" do
  runtimes []
end

This is outdated as far as Kitchenfiles goes. It still has the empty array runtimes setting which prevents Test Kitchen from attempting to run additional tests under RVM. We’ll remove this line, and update it for supporting the configurations of the recipes and features we want to test. The cookbook itself has three recipes:

  • default.rb
  • cacher-client.rb
  • cacher-ng.rb

By default, with no configurations defined in a Kitchenfile, test-kitchen will run the default recipe (using Chef Solo under Vagrant). This is useful in the common case, but we also want to actually test other functionality in the cookbook. In addition to the recipes, we want to verify that the LWRPs will do what we intend.

I updated the Kitchenfile with the following content:

cookbook "apt" do
  configuration "default"
  configuration "cacher-ng"
  configuration "lwrps"
end

A configuration can correspond to a recipe (default, cacher-ng), but it can also be arbitrarily named. This is a name used by kitchen test. The cacher-client recipe isn’t present because recipe[apt::cacher-ng] includes it, and getting the test to work, where the single node is a cacher client to itself, was prone to error. “I assure you, it works” :-). We’ll look at this later anyway.

With the above Kitchenfile, kitchen test will start up the Vagrant VMs and attempt to run Chef Solo with the recipes named by the configuration. This is a good start, but we want to actually run some minitest-chef tests. These will be created inside a “test” cookbook included with this cookbook. I created a cookbook named apt_test under ./test/kitchen/cookbooks using:

knife cookbook create apt_test -o ./test/kitchen/cookbooks

This creates the cookbook scaffolding like normal. I cleaned up the contents of the directory to contain what I needed to start:

test/kitchen/cookbooks//apt_test/metadata.rb
test/kitchen/cookbooks//apt_test/README.md
test/kitchen/cookbooks//apt_test/recipes
test/kitchen/cookbooks//apt_test/recipes/cacher-ng.rb
test/kitchen/cookbooks//apt_test/recipes/default.rb
test/kitchen/cookbooks//apt_test/recipes/lwrps.rb

The metadata.rb is as you’d expect, it contains the name, a version, maintainer information and a description. The README simply mentions that this is a test cookbook for the parent project. The recipes are the interesting part. Let’s address them in the order of the configurations in the Kitchenfile.

Configuration: default

First, the default recipe in the test cookbook. This is simply going to perform an include_recipe "apt::default". The way test kitchen runs, it will actually have the following run list for Chef Solo:

[test-kitchen::default, minitest-handler, apt_test::default, apt::default]

test-kitchen sets up some essential things for Test Kitchen itself. minitest-handler is the recipe that sets up minitest-chef-handler to run post-convergence tests. apt_test::default is the “test” recipe for this configuration, and finally apt::default is the cookbook’s recipe for this configuration named “default”.

Had we not done anything else here, the results are the same as simply running test kitchen with the original Kitchenfile (with runtimes, instead of configurations defined).

Minitest: default recipe

There are now minitest-chef tests for each configuration. The default recipe provides some “apt-get update” executes, and also creates a directory that can be used for preseeding packages. We’ll simply test that the preseeding directory exists. We could probably check that the cache is updated, but since this cookbook has worked for almost 4 years w/o issue for apt-get update we’ll trust it continues working :-). Here’s the test (ignoring the boilerplate):

  it 'creates the preseeding directory' do
    directory('/var/cache/local/preseeding').must_exist
  end

When Chef runs, it will run this test:

apt_test::default#test_0001_creates_the_preseeding_directory = 0.00 s = .
Finished tests in 0.007988s, 125.1808 tests/s, 125.1808 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Configuration: cacher-ng

Next, Test Kitchen runs the cacher-ng configuration. The recipe in the apt_test cookbook simply includes the apt::cacher-ng recipe. The run list in Chef Solo looks like this:

[test-kitchen::default, minitest-handler, apt_test::cacher-ng, apt::cacher-ng]

The apt::cacher-ng recipe also includes the client recipe, but basically does nothing unless the cacher_ipaddress attribute is set, or if we can search using a Chef Server (which Solo can’t, of course).

Minitest: cacher-ng recipe

The meat of the matter for the cacher-ng recipe is running the apt-cacher-ng service, so we’ve written a minitest test for this:

  it 'runs the cacher service' do
    service("apt-cacher-ng").must_be_running
  end

And when Chef runs the test:

apt_test::default#test_0001_runs_the_cacher_service = 0.06 s = .
Finished tests in 0.067250s, 14.8698 tests/s, 14.8698 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Configuration: lwrps

Finally, we have our custom configuration that doesn’t correspond to a recipe in the apt cookbook, lwrps. This configuration instead is to do a real-world integration test that the LWRPs actually do what they’re supposed to.

Recipe: apt_test::lwrps

The recipe itself looks like this:

include_recipe "apt"

apt_repository "opscode" do
  uri "http://apt.opscode.com"
  components ["main"]
  distribution "#{node['lsb']['codename']}-0.10"
  key "2940ABA983EF826A"
  keyserver "pgpkeys.mit.edu"
  action :add
end

apt_preference "chef" do
  pin "version 10.16.2-1"
  pin_priority "700"
end

The apt recipe is included because otherwise, we may not be able to notify the apt-get update resource to execute when the new sources.list is dropped off.

Next, we use Opscode’s very own apt repository as an example because we can rely on that existing. When Test Kitchen runs, it will actually write out the apt repository configuration file to /etc/apt/sources.list.d/opscode.list, but more on that in a minute.

Finally, we’re going to write out an apt preferences file for pinning the Chef package. Currently, Chef is actually packaged at various versions in Ubuntu releases:

% rmadison chef
      chef | 0.7.10-0ubuntu1.1 | lucid/universe | source, all
      chef | 0.8.16-4.2 | oneiric/universe | source, all
      chef |  10.12.0-2 | quantal/universe | source, all
      chef |  10.12.0-2 | raring/universe | source, all

So by adding the Opscode APT repository, and pinning Chef, we can ensure that we’re going to have the correct version of Chef installed as a package, if we were installing Chef as a package from APT :).

When Chef Solo runs, here is the run list:

[test-kitchen::default, minitest-handler, apt_test::lwrps]

Notice it doesn’t have “apt::lwrps”, since that isn’t a recipe in the apt cookbook.

Minitest: lwrps recipe

The minitest tests for the lwrps configuration and recipe look like this:

  it 'creates the Opscode sources.list' do
    file("/etc/apt/sources.list.d/opscode.list").must_exist
  end

  it 'adds the Opscode package signing key' do
    opscode_key = shell_out("apt-key list")
    assert opscode_key.stdout.include?("Opscode Packages <packages@opscode.com>")
  end

  it 'creates the correct pinning preferences for chef' do
    chef_policy = shell_out("apt-cache policy chef")
    assert chef_policy.stdout.include?("Package pin: 10.16.2-1")
  end

The first test simply asserts that the Opscode APT sources.list is present. We could elaborate on this by verifying that its content is correct, but for now we’re going to trust that the declarative resource in the recipe is, ahem, declared properly.

Next, we run the apt-key command to show the available GPG keys in the APT trusted keyring. This will have the correct Opscode Packages key if it was added correctly.

Finally, we test that the package pinning for the Chef package is correct. Successful output of the tests looks like this:

apt_test::default#test_0003_creates_the_correct_pinning_preferences_for_chef = 0.05 s = .
apt_test::default#test_0002_adds_the_opscode_package_signing_key = 0.05 s = .
apt_test::default#test_0001_creates_the_opscode_sources_list = 0.00 s = .
Finished tests in 0.112725s, 26.6133 tests/s, 26.6133 assertions/s.
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

The Real World Bits

The tests hide some of the detail. What does this actually look like on a real system? Glad you asked!

Here’s the sources.list for Opscode’s APT repository.

vagrant@ubuntu-12-04:~$ cat /etc/apt/sources.list.d/opscode.list
deb     http://apt.opscode.com precise-0.10 main

Next, the apt-key content:

vagrant@ubuntu-12-04:~$ sudo apt-key list
(snip, ubuntu's keys)
pub   1024D/83EF826A 2009-07-24
uid                  Opscode Packages <packages@opscode.com>
sub   2048g/3B6F42A0 2009-07-24

And the grand finale, the pinning preferences:

vagrant@ubuntu-12-04:~$ apt-cache policy chef
chef:
  Installed: 10.14.4-2.ubuntu.11.04
  Candidate: 10.16.2-1
  Package pin: 10.16.2-1
  Version table:
     10.16.2-1 700
        500 http://apt.opscode.com/ precise-0.10/main amd64 Packages
 *** 10.14.4-2.ubuntu.11.04 700
        100 /var/lib/dpkg/status

I used Opscode’s bento box for Ubuntu 12.04, which comes with the ‘omnibus’ Chef package version 10.14.4(-2.ubuntu.11.04). In order to install the newer Chef package and demonstrate the pinning, I’ll first remove it:

vagrant@ubuntu-12-04:~$ sudo dpkg --purge chef

Then, I install from the Opscode APT repository:

vagrant@ubuntu-12-04:~$ sudo apt-get install chef
...
Setting up chef (10.16.2-1) ...
...

And the package is installed:

vagrant@ubuntu-12-04:~$ apt-cache policy chef
chef:
  Installed: 10.16.2-1
  Candidate: 10.16.2-1
  Package pin: 10.16.2-1
  Version table:
 *** 10.16.2-1 700
        500 http://apt.opscode.com/ precise-0.10/main amd64 Packages
        100 /var/lib/dpkg/status

Currently the omnibus packages are NOT in the APT repository, since they do not have additional dependencies they are installed simply with dpkg. Don’t use this particular recipe if you’re using the Omnibus packages. Instead, just marvel at the utility of this. Perhaps instead, use the LWRPs in the apt cookbook to set up your own local APT repository and pinning preferences.

Conclusion

Test Kitchen is a framework for isolated integration testing in individual projects. As such, it has a lot of features, capabilities and also moving parts. Hopefully this post helps you understand some of them, and see how it works, and how you may be able to use it for yourself. Or, if you want, simply grab the apt_test::lwrps recipe’s contents and stick them in your own cookbook that manages Chef package installation and move along. :-)

All the code used in this post is available in the Opscode Cookbook’s organization “apt” repository.

Further Reading