TDD with Puppet
!
LOADays
2014-04-06
Antwerp, BE!
!
Garrett Honeycutt
@learnpuppet
gh@learnpuppet.com
http://learnpuppet.com
# whoami
Where are we
going?
• Why test?	

• What makes a good module	

• Tools	

• SetupVM	

• Hack	

• Travis-ci	

• More Hacking
3
LearnPuppet.com
• Training	

• 3 day Intro course	

• 2 day advanced course	

• Consulting	

• Auditing
4
Why test?
5
• Confidence to change things
Why test?
6
• Confidence to change things
• Know when you break something before deploying it
Why test?
7
• Confidence to change things
• Know when you break something before deploying it
• Quickly test a matrix of Puppet and Ruby versions
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
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
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
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.
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
What to test?
• Each parameter	

• Each resource	

• Ensure that failure occurs when that’s expected	

• Conditional logic
13
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
Semver
• Explanation of semantic versioning - semver.org
15
What is 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
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
18
Get VM
• Install VirtualBox - https://www.virtualbox.org/
• Install Vagrant - http://www.vagrantup.com/
• git clone https://github.com/ghoneycutt/learnpuppet-tdd-vagrant
• cd learnpuppet-tdd-vagrant
• vagrant up
21
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
RVM
http://www.rvm.io/	

!
• Allows you to easily switch between multiple versions of Ruby
23
Ruby Versions
• 1.8.7	

• 1.9.3	

• 2.0.0	

• 2.1.0 (Coming with Puppet v3.5.0)
24
rspec-puppet
http://rspec-puppet.com/ 	

Thanks,Tim!
25
Puppet Module
Skeleton
• $ git clone https://github.com/ghoneycutt/puppet-
module-skeleton
• $ mkdir -p `puppet config print vardir`/puppet-
module/skeleton/
• $ rsync -avp --exclude .git puppet-module-
skeleton/ `puppet config print vardir`/puppet-
module/skeleton/
26
Create a module
• generate motd module	

$ puppet module generate forgename-motd
27
Components
ghoneycutt-motd
ghoneycutt-motd/.fixtures.yml
ghoneycutt-motd/.gitignore
ghoneycutt-motd/.travis.yml
ghoneycutt-motd/Gemfile
ghoneycutt-motd/LICENSE
ghoneycutt-motd/Modulefile
ghoneycutt-motd/README.md
ghoneycutt-motd/Rakefile
ghoneycutt-motd/manifests
ghoneycutt-motd/manifests/init.pp
ghoneycutt-motd/spec
ghoneycutt-motd/spec/classes
ghoneycutt-motd/spec/classes/init_spec.rb
ghoneycutt-motd/spec/fixtures
ghoneycutt-motd/spec/fixtures/manifests
ghoneycutt-motd/spec/fixtures/manifests/site.pp
ghoneycutt-motd/spec/fixtures/modules
ghoneycutt-motd/spec/spec_helper.rb
28
Components
ghoneycutt-motd
ghoneycutt-motd/.fixtures.yml
ghoneycutt-motd/.gitignore
ghoneycutt-motd/.travis.yml
ghoneycutt-motd/Gemfile
ghoneycutt-motd/LICENSE
ghoneycutt-motd/Modulefile
ghoneycutt-motd/README.md
ghoneycutt-motd/Rakefile
ghoneycutt-motd/manifests
ghoneycutt-motd/manifests/init.pp
ghoneycutt-motd/spec
ghoneycutt-motd/spec/classes
ghoneycutt-motd/spec/classes/init_spec.rb
ghoneycutt-motd/spec/fixtures
ghoneycutt-motd/spec/fixtures/manifests
ghoneycutt-motd/spec/fixtures/manifests/site.pp
ghoneycutt-motd/spec/fixtures/modules
ghoneycutt-motd/spec/spec_helper.rb
29
.fixtures.yml
• List all of your dependencies from Modulefile
30
Gemfile
• Used by Bundler
31
.travis.yml
• Configure travis-ci.org
32
spec_helper.rb
• Code that is run before your spec tests.	

• Configures the spec testing environment.
33
Rakefile
• Validate syntax	

rake validate
!
• Validate style	

rake lint
34
Rakefile
• show all tasks	

rake -T
35
rake spec
• rake spec calls	

• rake spec_prep
• rake spec_standalone
• rake spec_clean
36
run tests
$ SPEC_OPTS="--format documentation" rake
spec_standalone
37
first test
it {
should contain_file('motd').with({
'ensure' => 'file',
'path' => '/etc/motd',
'owner' => 'root',
'group' => 'root',
'mode' => '0644',
'content' => nil,
})
}
38
run tests
• It fails! Now let’s fill in the code.
39
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
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
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
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
Exercise
Test all params
• All attributes of file resource should be configurable.	

• Write tests first.	

• Then add code to the module.
44
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
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
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
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
file content
# check for a specific line
!
it { should contain_file('ntp_conf').with_content(/^tinker panic 0$/) }
49
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
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
specify facts
context 'with default values for parameters on
EL 6' do
let(:facts) do
{ :osfamily => 'RedHat',
:lsbmajdistrelease => '6',
}
end
end
52
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
54
GitHub HowTo
Travis-ci.org
• Free!	

• Matrix testing	

• Integrates with GitHub	

• Tests every pull request automatically	

• Free!
55
.travis.yml---
env:
- PUPPET_VERSION=3.3.2
- PUPPET_VERSION=3.4.2
notifications:
email: false
rvm:
- 1.8.7
- 1.9.3
- 2.0.0
language: ruby
before_script: "gem install --no-ri --no-rdoc
bundler"
script: 'bundle exec rake validate && bundle exec
rake lint && SPEC_OPTS="--format documentation"
bundle exec rake spec'
gemfile: Gemfile
56
57
Integrate with Travis
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
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
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
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
Hashes
• https://github.com/ghoneycutt/puppet-module-vim/blob/master/
spec/classes/init_spec.rb
62
Exercise
refactor ntp
• Refactor ntp module to use a hash to specify differences between
OS’s
63
TDD with Puppet
!
LOADays
2014-04-06
Antwerp, BE!
!
Garrett Honeycutt
@learnpuppet
gh@learnpuppet.com
http://learnpuppet.com

20140406 loa days-tdd-with_puppet_tutorial