Test Driven Development (TDD) with Puppet Tutorial that was given at Cascadia IT Conference in Seattle on 2014-03-07 by Garrett Honeycutt of LearnPuppet.com.
Follow me at @learnpuppet
7. Why test?
7
• Confidence to change things
• Know when you break something before deploying it
• Quickly test a matrix of Puppet and Ruby versions
8. Why test?
8
• Confidence to change things
• Know when you break something before deploying it
• Quickly test a matrix of Puppet and Ruby versions
• Test all OS’s without having to deploy it everywhere
9. Why test?
9
• Confidence to change things
• Know when you break something before deploying it
• Quickly test a matrix of Puppet and Ruby versions
• Test all OS’s without having to deploy it everywhere
• Fast feedback
10. Why test?
10
• Confidence to change things
• Know when you break something before deploying it
• Quickly test a matrix of Puppet and Ruby versions
• Test all OS’s without having to deploy it everywhere
• Prevent regression of old problems
• Fast feedback
11. Why test?
11
• Confidence to change things
• Know when you break something before deploying it
• Quickly test a matrix of Puppet and Ruby versions
• Test all OS’s without having to deploy it everywhere
• Prevent regression of old problems
• Fast feedback
• Even in an agile world, we still have design specs.
12. Why test first?
• Puts a focus on what you want to accomplish.
• Documents the functionality that you care about.
• Makes you think about your design.
• Save time by building the minimum viable product first.
• You can refactor later.
12
13. What to test?
• Each parameter
• Each resource
• Ensure that failure occurs when that’s expected
• Conditional logic
13
14. What is actually
tested?
• Catalog is compiled with inputs such as setting values for
facts and parameters
• We test that things are or are not in the catalog
• Simple :)
14
16. 1.0.0
• README explains all parameters
• Passes lint
• Works with at least Ruby 1.8.7, 1.9.3, and 2.0.0
• Validates params
• Tests all params
• Tests all flows in logic
16
17. approach to
writing modules
• Write the README first, explaining all of your parameters and their
valid values and their default values.
• Add all of the parameters to your manifests with default values from
the README.
• Write the tests from the README.
• Write just enough code to get your tests to pass.
• Refactor as necessary.
17
21. Prep VM
• http://bit.ly/1fdJtwy
• What interface is detected? (ifconfig -a)
• Configure it for DHCP (/etc/sysconfig/network-scripts/
ifcfg-ethX)
• Restart the network (service network restart)
• Modify /etc/hosts and place your IP in there
• Validate (`ping puppet` should work)
• Restart apache (service httpd restart)
• # gem update -V rspec-core rspec
puppetlabs_spec_helper rspec-puppet
21
22. Testing tools
• Only if you are not using the providedVM
$ sudo gem install -V puppet-lint rspec rspec-puppet
puppetlabs_spec_helper --no-ri --no-rdoc
• https://github.com/puppetlabs/puppet-syntax-vim
• https://github.com/puppetlabs/puppet-syntax-emacs
22
40. testing params
• Each attribute of the file resource should be configurable
through params.
• Let’s test for values that should should work as well as what
should produce an error.
40
41. testing paramsdescribe 'with path specified' do
context 'as a valid path' do
let(:params) { { :path = '/usr/local/etc/motd' } }
!
it {
should contain_file('motd').with({
'path' = '/usr/local/etc/motd',
})
}
end
!
context 'as an invalid path' do
let(:params) { { :path = 'invalid/path' } }
!
it 'should fail' do
expect {
should contain_class('motd')
}.to raise_error(Puppet::Error)
end
end
41
42. testing file content
describe 'with content parameter specified' do
let(:params) { { :content = Welcome to
puppet.learnpuppet.comnnHave Fun!n } }
!
!
it {
should contain_file('motd').with_content(
%{Welcome to puppet.learnpuppet.com
!
Have Fun!
})
}
end
42
43. reading tests
$ grep -ie describe -e context spec/classes/init_spec.rb
describe 'motd' do
context 'with default values for all parameters' do
describe 'with motd_file parameter specified' do
context 'as a valid path' do
context 'as an invalid path' do
describe 'with motd_content parameter specified' do
43
44. Exercise
Test all params
• All attributes of file resource should be configurable.
• Write tests first.
• Then add code to the module.
44
45. four digit mode
describe 'with motd_mode specified' do
context 'as a valid four digit entry' do
let(:params) { { :mode = '0755' } }
!
it {
should contain_file('motd').with({
'mode' = '0755',
})
}
end
!
context 'as an invalid three digit entry' do
let(:params) { { :mode = '755' } }
!
it 'should fail' do
expect {
should contain_class('motd')
}.to raise_error(Puppet::Error,/^motd::mode must be a four digit
string./)
end
end
end
45
46. for loops
['666','66666','invalid',true].each do |mode|
context as invalid value #{mode} do
let(:params) { { :motd_mode = mode } }
!
it 'should fail' do
expect {
should contain_class('motd')
}.to raise_error(Puppet::Error,/^motd::mode must be a four
digit string./)
end
end
end
46
47. Exercise
Validate mode
• Validate mode with validate_re()
https://github.com/puppetlabs/puppetlabs-stdlib/tree/3.2.0#validate_re
• Test your regex at http://rubular.com/
47
48. resource relationships# package
it {
should contain_package('ntp_package').with({
...
})
}
!
# file
it {
should contain_file('ntp_config').with({
...
'require' = 'Package[ntp]',
})
}
!
# service
it {
should contain_service('ntp_service').with({
...
'subscribe' = 'File[ntp_config]',
})
}
48
49. file content
# check for a specific line
!
it { should contain_file('ntp_conf').with_content(/^tinker panic 0$/) }
49
50. file content
# what if the whole line is optional?
# in this case we test that it is not present
!
it { should_not contain_file('ntp_conf').with_content(/^tinker panic 0$/) }
50
51. Exercise
ntp module
• Use the last few slides to guide you on a module for NTP
• Do the minimum amount of work to get the tests to pass.
• Copy /etc/ntp.conf to your module as a starting place
51
52. specify facts
context 'with default values for parameters on
EL 6' do
let(:facts) do
{ :osfamily = 'RedHat',
:lsbmajdistrelease = '6',
}
end
end
52
53. Exercise
add OS to ntp
• Add support for another OS.This OS should have at least a
different name for the package or service.
53
58. Test functions
# lib/puppet/parser/functions/yell.rb
module Puppet::Parser::Functions
newfunction(:yell, :type = :rvalue, :doc = -EOS
Takes one argument, a string to be capitalized. Returns the
string in
all caps.
EOS
) do |args|
raise(Puppet::ParseError, yell(): Wrong number of arguments +
given (#{args.size} for 1)) if args.size != 1
args[0].upcase
end
end
58
59. Test functions
# spec/functions/yell_spec.rb
require 'spec_helper'
describe 'yell' do
it 'should run with correct number of arguments (1)' do
should run.with_params('hello world').and_return('HELLO WORLD')
end
!
it 'should fail with no arguments' do
should run.with_params().and_raise_error(Puppet::ParseError)
end
!
it 'should fail with more than one argument (2)' do
should run.with_params('too','many').and_raise_error(Puppet::ParseError)
end
end
59
60. Defines# spec/defines/mkdir_p_spec.rb
require 'spec_helper'
describe 'common::mkdir_p' do
context 'should create new directory' do
let(:title) { '/some/dir/structure' }
!
it {
should contain_exec('mkdir_p-/some/dir/structure').with({
'command' = 'mkdir -p /some/dir/structure',
'unless' = 'test -d /some/dir/structure',
})
}
end
!
context 'should fail with a path that is not absolute' do
let(:title) { 'not/a/valid/absolute/path' }
!
it do
expect {
should contain_exec('mkdir_p-not/a/valid/absolute/path').with({
'command' = 'mkdir -p not/a/valid/absolute/path',
'unless' = 'test -d not/a/valid/absolute/path',
})
}.to raise_error(Puppet::Error)
end
end
end
60
61. Exercise
Defines
• Create a define,‘say’, that takes a param,‘msg’ or if msg is not sent,
use the title and pass that to a notify{} resource.
• Write tests first, then write the define.
• Bonus to create your own function to run on the msg, such as
making it all lower case or l33t sp34k.
61