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 Infra Client run. ChefSpec tests execute 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 Chef Workstation. 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 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 of the core Chef Infra Client resources such as guards, attributes, multiple actions. 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: '20.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: '20.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: '20.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: '20.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: '20.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: '20.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: '20.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: '20.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
More Information
For more information about ChefSpec: