Il y a 5 ans -

Temps de lecture 8 minutes

Puppet Recipes 03 : Tests Unitaires

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 avec bundler 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::installntp::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 dossier spec/hosts) ou les fonctions (dans spec/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 !

Publié par Matthieu Nantern

Matthieu est un consultant Java senior spécialisé en DevOps, Apache Cassandra et la performance des applications. Depuis plus de 8 ans il remplit des missions agiles de développement, d'architecture et de conseils pour diverses sociétés. Récemment il est devenu formateur sur Apache Cassandra. Il travaille actuellement sur un projet de développement d'une plateforme IoT pour un grand groupe français.

Publié par Xebia France

Xebia est un cabinet de conseil international spécialisé dans les technologies Big Data, Web, les architectures Java et la mobilité dans des environnements agiles. Depuis plus de 15 ans, nous avons la volonté de partager notre expertise et nos actualités à travers notre blog technique.

Commentaire

1 réponses pour " Puppet Recipes 03 : Tests Unitaires "

  1. Publié par , Il y a 5 ans

    J’ai toujours été indécis à propos des tests unitaires pour les modules Puppet, surtout pour les modules simples. J’ai vraiment l’impression que le test ne sert à rien, car en pratique tu te retrouves par exemple à copier la déclaration de « file » du module dans ton test, et rien de plus.

    Au final les tests sont plus ou moins un copier/coller du module avec un peu de boilerplate, et deviennent une plaie à maintenir…

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.