TDD with Puppet Tutorial presented at Cascadia IT Conference 2014-03-07
-
Upload
garrett-honeycutt -
Category
Technology
-
view
471 -
download
2
description
Transcript of TDD with Puppet Tutorial presented at Cascadia IT Conference 2014-03-07
TDD with Puppet !
Cascadia IT Conference 2014-03-08 Seattle, WA!
!Garrett Honeycutt
@learnpuppet [email protected] http://learnpuppet.com
Where are we going?
• Why test?
• What makes a good module
• Tools
• Setup VM
• Hack
• Travis-ci
• More Hacking
���3
LearnPuppet.com
• Training
• 3 day Intro course
• 2 day advanced course
• Consulting
• Auditing
���4
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
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
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
Testing tools
• Only if you are not using the provided VM
$ 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
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
Componentsghoneycutt-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
Componentsghoneycutt-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
spec_helper.rb
• Code that is run before your spec tests.
• Configures the spec testing environment.
���33
first test it {
should contain_file('motd').with({
'ensure' => 'file',
'path' => '/etc/motd',
'owner' => 'root',
'group' => 'root',
'mode' => '0644',
'content' => nil,
})
}
���38
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 params describe '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.com\n\nHave 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
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
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
TDD with Puppet !
Cascadia IT Conference 2014-03-08 Seattle, WA!
!Garrett Honeycutt
@learnpuppet [email protected] http://learnpuppet.com