ChefSpec¶
Use ChefSpec to simulate the convergence of resources on a node:
- Is an extension of RSpec, a behavior-driven development (BDD) framework for Ruby
- Is the fastest way to test resources and recipes
ChefSpec is a framework that tests resources and recipes as part of a simulated chef-client run. ChefSpec tests execute very quickly. When used as part of the cookbook authoring workflow, ChefSpec tests are often the first indicator of problems that may exist within a cookbook.
Run ChefSpec¶
ChefSpec is packaged as part of the Chef development kit. To run ChefSpec:
$ chef exec rspec
Unit Tests¶
RSpec is a behavior-driven development (BDD) framework that uses a natural language domain-specific language (DSL) to quickly describe scenarios in which systems are being tested. RSpec allows a scenario to be set up, and then executed. The results are compared to a set of defined expectations.
ChefSpec is built on the RSpec DSL.
Syntax¶
The syntax of RSpec-based tests should follow the natural language descriptions of RSpec itself. The tests themselves should create an English-like sentence: “The sum of one plus one equals two, and not three.” For example:
describe '1 plus 1' do
it 'equals 2' do
a = 1
b = 1
sum = a + b
expect(sum).to eq(2)
expect(sum).not_to eq(3)
end
end
where:
describe
creates the testing scenario:1 plus 1
it
is a block that defines a list of parameters to test, along with parameters that define the expected outcomedescribe
andit
should have human readable descriptions: “one plus one equals two”a
,b
, andsum
define the testing scenario:a
equals one,b
equals one, thesum
of one plus equals twoexpect()
defines the expectation: the sum of one plus one equals two—expect(sum).to eq(2)
—and does not equal three–expect(sum).not_to eq(3)
.to
tests the results of the test for true;.not_to
tests the result of the test for false; a test passes when the results of the test are true
context¶
RSpec-based tests may contain context
blocks. Use context
blocks within describe
blocks to define “tests within tests”. Each context
block is tested individually. All context
blocks within a describe
block must be true for the test to pass. For example:
describe 'math' do
context 'when adding 1 + 1' do
it 'equals 2' do
expect(sum).to eq(2)
end
end
context 'when adding 2 + 2' do
it 'equals 4' do
expect(sum).to eq(4)
end
end
end
where each context
block describes a different testing scenario: “The sum of one plus one to equal two, and also the sum of two plus two to equal four.” A context
block is useful to handle platform-specific scenarios. For example, “When on platform A, test for foo; when on platform B, test for bar.” For example:
describe 'cookbook_name::recipe_name' do
context 'when on Debian' do
it 'equals 2' do
a = 1
b = 1
sum = a + b
expect(sum).to eq(2)
end
end
context 'when on Ubuntu' do
it 'equals 2' do
expect(1 + 1).to eq(2)
end
end
context 'when on Windows' do
it 'equals 3' do
expect(1 + 2).to eq(3)
end
end
end
let¶
RSpec-based tests may contain let
statements within a context
block. Use let
statements to create a symbol, assign it a value, and then use it elsewhere in the context
block. For example:
describe 'Math' do
context 'when adding 1 + 1' do
let(:sum) { 1 + 1 }
it 'equals 2' do
expect(sum).to eq(2)
end
end
context 'when adding 2 + 2' do
let(:sum) do
2 + 2
end
it 'equals 4' do
expect(sum).to eq(4)
end
end
end
where:
- The first
let
statement creates the:sum
symbol, and then assigns it the value of one plus one. Theexpect
statement later in the test usessum
to test that one plus one equals two - The second
let
statement creates the:sum
symbol, and then assigns it the value of two plus two. Theexpect
statement later in the test usessum
to test that two plus two equals four
Require ChefSpec¶
A ChefSpec unit test must contain the following statement at the top of the test file:
require 'chefspec'
Examples¶
The ChefSpec repo on github has an impressive collection of examples. For all of the core chef-client resources, for guards, attributes, multiple actions, and so on. Take a look at those examples and use them as a starting point for building your own unit tests. Some of them are included below, for reference here.
file Resource¶
Recipe
file '/tmp/explicit_action' do
action :delete
end
file '/tmp/with_attributes' do
user 'user'
group 'group'
backup false
action :delete
end
file 'specifying the identity attribute' do
path '/tmp/identity_attribute'
action :delete
end
Unit Test
require 'chefspec'
describe 'file::delete' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'deletes a file with an explicit action' do
expect(chef_run).to delete_file('/tmp/explicit_action')
expect(chef_run).to_not delete_file('/tmp/not_explicit_action')
end
it 'deletes a file with attributes' do
expect(chef_run).to delete_file('/tmp/with_attributes').with(backup: false)
expect(chef_run).to_not delete_file('/tmp/with_attributes').with(backup: true)
end
it 'deletes a file when specifying the identity attribute' do
expect(chef_run).to delete_file('/tmp/identity_attribute')
end
end
template Resource¶
Recipe
template '/tmp/default_action'
template '/tmp/explicit_action' do
action :create
end
template '/tmp/with_attributes' do
user 'user'
group 'group'
backup false
end
template 'specifying the identity attribute' do
path '/tmp/identity_attribute'
end
Unit Test
require 'chefspec'
describe 'template::create' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'creates a template with the default action' do
expect(chef_run).to create_template('/tmp/default_action')
expect(chef_run).to_not create_template('/tmp/not_default_action')
end
it 'creates a template with an explicit action' do
expect(chef_run).to create_template('/tmp/explicit_action')
end
it 'creates a template with attributes' do
expect(chef_run).to create_template('/tmp/with_attributes').with(
user: 'user',
group: 'group',
backup: false,
)
expect(chef_run).to_not create_template('/tmp/with_attributes').with(
user: 'bacon',
group: 'fat',
backup: true,
)
end
it 'creates a template when specifying the identity attribute' do
expect(chef_run).to create_template('/tmp/identity_attribute')
end
end
package Resource¶
Recipe
package 'explicit_action' do
action :remove
end
package 'with_attributes' do
version '1.0.0'
action :remove
end
package 'specifying the identity attribute' do
package_name 'identity_attribute'
action :remove
end
Unit Test
require 'chefspec'
describe 'package::remove' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'removes a package with an explicit action' do
expect(chef_run).to remove_package('explicit_action')
expect(chef_run).to_not remove_package('not_explicit_action')
end
it 'removes a package with attributes' do
expect(chef_run).to remove_package('with_attributes').with(version: '1.0.0')
expect(chef_run).to_not remove_package('with_attributes').with(version: '1.2.3')
end
it 'removes a package when specifying the identity attribute' do
expect(chef_run).to remove_package('identity_attribute')
end
end
chef_gem Resource¶
Recipe
chef_gem 'default_action'
chef_gem 'explicit_action' do
action :install
end
chef_gem 'with_attributes' do
version '1.0.0'
end
chef_gem 'specifying the identity attribute' do
package_name 'identity_attribute'
end
Unit Test
require 'chefspec'
describe 'chef_gem::install' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'installs a chef_gem with the default action' do
expect(chef_run).to install_chef_gem('default_action')
expect(chef_run).to_not install_chef_gem('not_default_action')
end
it 'installs a chef_gem with an explicit action' do
expect(chef_run).to install_chef_gem('explicit_action')
end
it 'installs a chef_gem with attributes' do
expect(chef_run).to install_chef_gem('with_attributes').with(version: '1.0.0')
expect(chef_run).to_not install_chef_gem('with_attributes').with(version: '1.2.3')
end
it 'installs a chef_gem when specifying the identity attribute' do
expect(chef_run).to install_chef_gem('identity_attribute')
end
end
directory Resource¶
Recipe
directory '/tmp/default_action'
directory '/tmp/explicit_action' do
action :create
end
directory '/tmp/with_attributes' do
user 'user'
group 'group'
end
directory 'specifying the identity attribute' do
path '/tmp/identity_attribute'
end
Unit Test
require 'chefspec'
describe 'directory::create' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'creates a directory with the default action' do
expect(chef_run).to create_directory('/tmp/default_action')
expect(chef_run).to_not create_directory('/tmp/not_default_action')
end
it 'creates a directory with an explicit action' do
expect(chef_run).to create_directory('/tmp/explicit_action')
end
it 'creates a directory with attributes' do
expect(chef_run).to create_directory('/tmp/with_attributes').with(
user: 'user',
group: 'group',
)
expect(chef_run).to_not create_directory('/tmp/with_attributes').with(
user: 'bacon',
group: 'fat',
)
end
it 'creates a directory when specifying the identity attribute' do
expect(chef_run).to create_directory('/tmp/identity_attribute')
end
end
Guards¶
Recipe
service 'true_guard' do
action :start
only_if { 1 == 1 }
end
service 'false_guard' do
action :start
not_if { 1 == 1 }
end
service 'action_nothing_guard' do
action :nothing
end
Unit Test
require 'chefspec'
describe 'guards::default' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'includes resource that have guards that evaluate to true' do
expect(chef_run).to start_service('true_guard')
end
it 'excludes resources that have guards evaluated to false' do
expect(chef_run).to_not start_service('false_guard')
end
it 'excludes resource that have action :nothing' do
expect(chef_run).to_not start_service('action_nothing_guard')
end
end
include_recipe Method¶
Recipe
include_recipe 'include_recipe::other'
Unit Test
require 'chefspec'
describe 'include_recipe::default' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'includes the `other` recipe' do
expect(chef_run).to include_recipe('include_recipe::other')
end
it 'does not include the `not` recipe' do
expect(chef_run).to_not include_recipe('include_recipe::not')
end
end
Multiple Actions¶
Recipe
service 'resource' do
action :start
end
service 'resource' do
action :nothing
end
Unit Test
require 'chefspec'
describe 'multiple_actions::sequential' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04', log_level: :fatal).converge(described_recipe) }
it 'executes both actions' do
expect(chef_run).to start_service('resource')
end
it 'does not match other actions' do
expect(chef_run).to_not disable_service('resource')
end
end