Qui dit Puppet dit "Infrastructure as a code" et donc code. Avec tout ce qui vient avec: la séparation du code et des données mais aussi les tests unitaires.
Les tests unitaires sont une étape primordiale dans le développement de nos modules. Au fur et à mesure que ceux-ci vont s’étoffer, gérer de multiples OS et de plus en plus de fonctionnalités, les tests unitaires nous protègent contre les régressions, facilitent le refactoring et permettent de tester automatiquement les nouvelles versions de Puppet. Pour tester nos modules Puppet, nous allons utiliser rspec-puppet
.
Installation de notre environnement de tests unitaires
Nous allons avoir besoin de mettre en place notre environnement. Le plus simple à faire est d’installer vagrant puis d’exécuter les commandes suivantes:
# Récupère la configuration Vagrant pour Puppet $ git clone https://github.com/ghoneycutt/learnpuppet-tdd-vagrant $ cd learnpuppet-tdd-vagrant # Lance la VM via vagrant (allez nourrir le poney, ça peut prendre du temps…) $ vagrant up # Ouvre une console dans la VM de test $ vagrant ssh
Notre module de test: NTP
Notre environnement étant maintenant en place, nous allons récupérer notre module de test (à exécuter depuis la VM Vagrant):
$ git clone https://github.com/mNantern/xebia-puppet-tu-ntp.git $ cd xebia-puppet-tu-ntp
Il s’agit du module ntp originel de Puppetlabs mais modifié pour utiliser Hiera et un fichier defaults.yaml
comme indiqué dans le précédent article. Deux fichiers sont très importants pour nos tests unitaires:
- Le fichier
Gemfile
qui déclare la liste des gems nécessaires pour notre module et permet de les installer simplement avecbundler
via la commande:bundle install
. Pour notre VM, il n’y a pas besoin de cela, toutes les gems sont déjà installées. - Le fichier
.fixtures.yml
qui permet de préciser la liste des dépendances de notre module. Il est possible de fournir le lien d’un dépôt git ou l’emplacement physique de notre dépendance.
Notre fichier Gemfile
ressemble à cela:
source "https://rubygems.org" puppetversion = ENV.key?('PUPPET_VERSION') ? "= #{ENV['PUPPET_VERSION']}" : ['= 3.3.1'] gem 'puppet', puppetversion gem 'puppetlabs_spec_helper', '>= 0.1.0' gem 'puppet-lint', '>= 0.3.2' gem 'facter', '>= 1.7.0', "< 1.8.0"
Quant à notre fichier .fixtures.yml
:
fixtures: repositories: stdlib: repo: 'git://github.com/puppetlabs/puppetlabs-stdlib.git' ref: '4.1.0' symlinks: ntp: "#{source_dir}"
Le module stdlib
étant une dépendance de notre module, il va alors être téléchargé à chaque lancement de tests unitaires dans le dossier spec/fixtures/modules
. Si l’on ne souhaite pas le télécharger à chaque fois, il est possible de le cloner puis d’indiquer son emplacement dans le fichier .fixtures.yml
. Par exemple:
fixtures: symlinks: "ntp": "#{source_dir}" "stdlib": "#{source_dir}/../puppetlabs-stdlib"
Pour vérifier que notre environnement est bien en place on exécute la commande suivante qui va lancer l’exécution de l’ensemble des tests unitaires de notre module:
$ rake spec
Et le résultat:
$ rake spec Initialized empty Git repository in /root/modules/module-ntp/spec/fixtures/modules/stdlib/.git/ remote: Reusing existing pack: 4697, done. remote: Counting objects: 41, done. remote: Compressing objects: 100% (36/36), done. remote: Total 4738 (delta 10), reused 15 (delta 0) Receiving objects: 100% (4738/4738), 955.07 KiB | 752 KiB/s, done. Resolving deltas: 100% (1761/1761), done. HEAD is now at e1f2a93 Update Modulefile, CHANGELOG for 3.2.0 /usr/local/rvm/rubies/ruby-1.9.3-p545/bin/ruby -S rspec spec/classes/init_spec.rb --color ...... Finished in 0.9666 seconds 7 examples, 0 failures
Test 1 : Classes et relations
Notre environnement étant maintenant en place, nous pouvons créer notre premier test.
Voici le contenu de notre fichier manifests/init.pp
(abrégé):
class { '::ntp::install': } -> class { '::ntp::config': } ~> class { '::ntp::service': }
Nous allons donc ajouter un test afin de vérifier que notre class ntp
contient bien les classes ntp::install
, ntp::config
et ntp::service
.
Ici, nous ne testons que le contenu de notre classe ntp
. Nous testerons plus en détail chacune des sous-classes. Les tests correspondant au fichier init.pp
sont placés dans le fichier spec/classes/init_spec.pp
qui contient déjà le test suivant:
['AIX','Debian', 'RedHat','SuSE', 'FreeBSD', 'Archlinux', 'Gentoo'].each do |system| context "on #{system}" do let(:facts) {{ :osfamily => system }} it { should contain_class('ntp') } end end
Ce test vérifie deux choses :
- tout d’abord que le catalogue Puppet compile (qu’il n’y a pas d’erreur)
- puis que le résultat contient bien la classe principale, à savoir
ntp
, et cela pour nos 7 systèmes d’exploitation pris en charge.
Voici ce qu’il faut ajouter afin de vérifier que notre fichier init.pp
contient bien les classes ntp::install
, ntp::config
et ntp::service
:
['AIX','Debian', 'RedHat','SuSE', 'FreeBSD', 'Archlinux', 'Gentoo'].each do |system| context "on #{system}" do let(:facts) {{ :osfamily => system }} it { should contain_class('ntp') } it { should contain_class('ntp::install').that_comes_before('Class[ntp::config]') } it { should contain_class('ntp::config').that_notifies('Class[ntp::service]') } it { should contain_class('ntp::service') } end end
Rien de bien surprenant, nous vérifions que notre catalogue contient nos trois classes et nous vérifions en plus l’ordre d’exécution de ces classes. La classe ntp::install
doit arriver avant la classe ntp::config
dans le catalogue. Et celle-ci doit notifier la classe ntp::service
une fois son exécution terminée.
Et le résultat :
$ rake spec HEAD is now at e1f2a93 Update Modulefile, CHANGELOG for 3.2.0 /usr/local/rvm/rubies/ruby-1.9.3-p545/bin/ruby -S rspec spec/classes/init_spec.rb --color ............................ Finished in 1.16 seconds 28 examples, 0 failures
Test 2 : la ressource file
Nous allons maintenant ajouter des tests pour la classe ntp::config
:
class ntp::config ( [...] ){ if $keys_enable { $directory = dirname($keys_file) file { $directory: ensure => directory, owner => 0, group => 0, mode => '0755', recurse => true, } } file { $config: ensure => file, owner => 0, group => 0, mode => '0644', content => template($config_template), } }
Dans le fichier spec/classes/config_spec.rb
, nous allons tout d’abord tester le cas le plus simple : que le fichier $config
est bien présent et contient les paramètres attendus.
it do should contain_file('/etc/ntp.conf').with( 'ensure' => 'file', 'owner' => 0, 'group' => 0, 'mode' => '0644', 'content' => /pool\.ntp\.org/ ) end
Un point important à noter est que pour le paramètre content
, on vérifie ici une expression régulière sur le contenu du fichier qui en l’occurrence doit contenir la chaine pool.ntp.org
.
Deuxième test, on vérifie que le dossier $directory
est bien créé. Ici la valeur de cette variable dépend de l’OS testé, nous allons donc la récupérer directement depuis notre fichier defaults.yaml
:
context "on #{system} with keys_enable" do hiera = Hiera.new(:config => 'spec/fixtures/hiera/hiera.yaml') config = hiera.lookup("ntp_#{system}_conf",nil,nil) keys_file = config['keysfile'] directory = File.dirname(keys_file) let (:params) {{:keys_enable => true, :keys_file => keys_file}} it do should contain_file(directory).with( 'ensure' => 'directory', 'owner' => 0, 'group' => 0, 'mode' => '0755', 'recurse' => true ) end end
Dans le cas présent, on récupère la valeur keys_file
depuis le fichier defaults.yaml,
puis on la fournit en tant que paramètre d’entrée à notre classe ntp::config
.
Et le résultat:
.......................................... Finished in 1.38 seconds 42 examples, 0 failures
L’ensemble du code ci-dessus est disponible sur la branche resultat
du dépôt Git.
Aller plus loin dans les tests
Nous n’avons vu ici que quelques-uns des tests que l’on peut effectuer sur nos modules, mais il est possible d’en réaliser bien plus :
- Tester les erreurs renvoyées par Puppet avec
expect … to raise_error()
- Tester les defines (à ajouter dans le dossier
spec/defines
), les hosts (dans le dossierspec/hosts
) ou les fonctions (dansspec/functions
). - Compter le nombre de ressources dans le catalogue résultat.
Pour plus d’informations il convient de se référer à la documentation disponible sur le site rspec-puppet.com.
Et la couverture de code ?
L’une des choses que l’on aime bien avoir avec nos tests unitaires est la couverture de ces tests. Sur l’ensemble de notre code, quel pourcentage est vérifié par (au moins) un test ?
Rspec-puppet fourni cette information en ajoutant la ligne suivante dans le fichier spec/spec_helper.rb
:
at_exit { RSpec::Puppet::Coverage.report! }
Attention : pour que cela fonctionne, il vous faudra la dernière version du dépôt Git, cette fonction n’étant pas encore disponible dans une gem. Et le résultat :
.......................................... Finished in 1.48 seconds 42 examples, 0 failures Total resources: 12 Touched resources: 7 Resource coverage: 58.33% Untouched resources: Anchor[ntp::begin] Anchor[ntp::end] Class[Ntp::Params] Package[ntp] Service[ntp]
Peut mieux faire, mais maintenant vous avez le nécessaire pour améliorer cette couverture. À vous de jouer ! Si vous avez lu jusqu’ici et que le sujet vous intéresse, n’hésitez pas, Xebia recrute !