Blog tech

MiniTest - Un framework de test léger et rapide

Rédigé par Nicolas Cavigneaux | 29 novembre 2011

Je ne vais pas revenir sur l’importance de mettre en place des batteries de tests automatisés pour votre application. Vous savez déjà qu’avoir une suite de tests bien pensée permet de s’assurer du bon fonctionnement de votre code, de sa solidité, d’être serein lors des demandes d’évolution ou de refactorisation. Les tests vous permettent également de limiter au maximum les régressions ainsi que les (ré-)introductions de bugs.

Peut-être que vous n’utilisez pas encore les tests par manque de temps, par peur de l’inconnu ou de la difficulté associée à l’écriture de tests. N’en reste pas moins que je suis sûr que vous captez l’intérêt capital des tests sur des projets amenés à beaucoup évoluer.

MiniTest est une des alternatives disponible dans l’éco-système Ruby quand il s’agit de mettre en place des tests. MiniTest est le digne héritier de Test::Unit. Si vous utilisez Ruby 1.9 et que vous avez mis en place des tests à l’aide de Test::Unit, vous utilisez déjà MiniTest de manière totalement transparente.

En effet, l’un des changement majeur entre Ruby 1.8 et 1.9 est le framework de test livré avec l’interpréteur. Depuis Ruby 1.9, le vénérable Test::Unit (qui rappelons le a initié beaucoup de développeurs à l’écriture de tests) à été remplacé par MiniTest.

Nous allons donc voir pourquoi ce changement de framework a eu lieu et en quoi cette nouvelle alternative est intéressante.

Pourquoi MiniTest ?

Test::Unit a permis a beaucoup de développeurs de découvrir le monde des tests en parallèle de Ruby, ce qui est une bonne chose puisque quasi tous les développeurs Ruby sont maintenant convaincus qu’écrire des tests est une bonne pratique (même s’ils ne le mettent pas tous en œuvre).

Seulement, très rapidement, les limites de Test::Unit se sont fait sentir :

  • lenteur
  • conception tortueuse avec l’intégration d’outils très peu utilisés (interface Gtk, FxRuby, …)
  • peu orienté objet
  • pas de support des mocks (simulation d’objets).

Voici ce à quoi ressemble un test Test::Unit :

class TestUser < Test::Unit::TestCase
  def test_user_name_not_nil
    user = User.new("nico")
    assert user.name
    assert_equal user.name, "nico"
    assert_nil user.birthday
  end
end

Les développeurs Ruby se sont donc tournées vers des solutions tierces qui répondent mieux à leur attentes tels que RSpec, Mocha ou Shoulda.

Avec l’arrivée de Ruby 1.9, il était évident qu’une refonte de ce framework de test était nécessaire. C’est ici que MiniTest fait son entrée. Première chose très intéressante, MiniTest a été conçu de telle façon qu’il est compatible a quasiment 100% avec les tests Test::Unit. Si vous avez déjà des tests développés à l’aide de Test::Unit et que vous voulez passer à MiniTest vous n’aurez certainement rien à changer dans vos tests existants.

D’ailleurs si vous utilisez Ruby 1.9, vous utilisez déjà MiniTest même si vous pensez utiliser Test::Unit. Les méthodes de Test::Unit sont devenus de simples proxys vers les méthodes de MiniTest. Pour les utilisateurs de Ruby 1.8, il vous faudra charger le gem minitest pour sauter le pas.

Quels sont les avantages ?

Pourquoi changer me direz vous puisque du code Test::Unit fonctionne parfaitement avec MiniTest. En premier lieu, pour la rapidité. En effet MiniTest est beaucoup plus léger et rapide à l’exécution ce qui vous fera gagner du temps à chaque lancement de vos tests.

MiniTest fourni une suite d’outils très complète permettant de faire du TDD, du BDD, du mocking, ou encore du benchmarking.

MiniTest est livré avec les outils suivants :

  • minitest/autorun : permet de lancer automatiquement vos tests
  • minitest/unit : API de tests unitaires
  • minitest/spec : API de BDD
  • minitest/mock : API d’objet factices (mocks)
  • minitest/benchmark : API de test de performance
  • minitest/pride : coloration de vos résultats de test

Comment profiter de MiniTest

Vous pouvez commencer par installer le gem si vous utilisez Ruby 1.8

sudo gem install minitest

Si vous utilsez Ruby 1.9, MiniTest est déjà utilisable. Vous pouvez toutefois installer le gem pour être sûr d’utiliser la dernière version.

Maintenant dans votre code :

require 'rubygems'
gem 'minitest' # on utilise la dernière version
require 'minitest/autorun' # assure le lancement de vos suites de test

Tests unitaires

minitest/unit a pour avantage d’être une librairie très petite et vraiment très rapide. Tout ce que vous pouviez faire avec Test::Unit est faisable avec MiniTest et même plus. Vous utiliserez donc des assertions pour définir le comportement attendu de votre code :

require 'minitest/autorun'

class TestUser < MiniTest::Unit::TestCase
  def setup
    @user = User.new(:firstname => "Nicolas", :lastname => "Cavigneaux")
  end

  def test_user_fullname
    assert_equal "Nicolas Cavigneaux", @user.fullname
  end
end

Ordre de jeu aléatoire

MiniTest inclut le tirage au hasard de l’ordre d’exécution des tests. Lorsque vous lancez votre suite de tests avec Test::Unit, l’ensemble de vos tests sont toujours jouées dans le même ordre (ordre alphabétique des noms de méthode) ce qui peut masquer des bugs de dépendance à l’ordre de jeu. Lorsque vous lancez une suite avec MiniTest, vous verrez à la fin une ligne du type :

Test run options: --seed 1234

Cette ligne vous permet de connaître le seed utilisé pour déterminer l’ordre de jeu de vos tests. Vos tests sont donc joués dans un ordre aléatoire ce qui permet de s’affranchir des bugs de dépendance. Si vous obtenez un test qui ne plante que de temps à autres, vous pouvez alors considérer qu’il y a un bug de dépendance et le seed vous permettra de relancer la suite dans les mêmes conditions :

rake TESTOPTS="--seed=1234" test

Vous pouvez jouer vos tests dans un ordre fixe bien que ce soit fortement déconseillé. Si vous dépendez de l’ordre d’exécution dans vos tests, c’est un signe de mauvaise conception. Pour forcer un ordre fixe une méthode de classe existe mais annonce la couleur :

i_suck_and_my_tests_are_order_dependent!()

Ignorer des tests

MiniTest intègre une méthode qui permet d’ignorer les tests d’une méthode de manière simple :

def test_foo
  skip("Buggy test")
  assert_equal false, true
end

Si des tests sont ignorées, ils seront tout de même recensés dans le résultat :

1 tests, 0 assertions, 0 failures, 0 errors, 1 skips

Verbosité

Il arrive souvent que nos tests deviennent lents, soit parce qu’ils sont très nombreux, soit parce qu’ils sont mal écrits. MiniTest propose d’afficher pour chaque test, le temps passé ce qui permet de très facilement retrouver les tests lents :

rake TESTOPTS="-v"

Foo#test_bar: 0.02 s: .
Foo#test_foo: 0.01 s: .
Foo#test_baz: 0.15 s: .

Mocking

MiniTest inclut également une API de mocking qui permet de créer des objets factices à utiliser dans les tests. Le mocking est quasiment indispensable aux tests et c’est une bonne chose d’avoir cette API intégrée directement à la librairie standard. Nous n’avons donc plus besoin d’avoir recours à des librairies externes (Mocha, Factory Girl) pour simuler des objets.

require 'minitest/autorun'

describe Foo do
  before do
    @foo = MiniTest::Mock.new
  end

  describe "#bar?" do
    describe "asked if bar?" do
      it "should return false" do
        @foo.expect :bar?, false
        @foo.bar? # => false
      end
    end
  end
end

BDD

Dernière chose mais non des moindres, MiniTest, en plus d’être un remplaçant plus rapide et plus fourni que Test::Unit, il s’invite également sur le terrain de jeu de Framework de test comme RSpec. En effet, MiniTest offre un DSL BDD qui vous permet d’écrire vos tests de manière plus élégante :

require 'minitest/spec'

describe User do
  before do
    @user = User.new(:firstname => "Nicolas", :lastname => "Cavigneaux")
  end

  describe "when asked for fullname" do
    it "should respond with firtname and lastname" do
      @user.fullname.must_equal "Nicolas Cavigneaux"
    end
  end
end

minitest/spec propose toutes les fonctionnalités nécessaires à l’écriture de specs. minitest/spec se base sur minitest/unit et convertit vos specs en assertions.

Pour ma part, j’y ai tout de suite vu le moyen de m’affranchir de RSpec et donc d’éviter des dépendances relativement lourdes uniquement pour les tests. Je n’ai pas encore été limité par les possibilités BDD de MiniTest. Les experts de RSpec trouveront peut-être des choses manquantes mais ne seront pas chamboulés dans leurs habitudes.

Benchmark

minitest/benchmark est un moyen pratique de tester les performances de votre application et de vos algorithmes. Vous pourrez donc vous assurer qu’une méthode donnée ne passe jamais d’un algorithme constant à un algorithme exponentiel par exemple.

describe User do
  bench_performance_constant "generate_family_tree" do |n|
    100.times do
      @user.generate_family_tree
    end
  end
end

vous donnera quelque chose comme :

# Running benchmarks:

generate_family_tree   0.006167  0.079279  0.786993

Conclusion

Si vous débutez dans le TDD ou le BDD, vous devriez vraiment commencer par jeter un œil à MiniTest qui a l’avantage d’être disponible de base avec Ruby 1.9 et qui s’avère très complet. Vous aurez accès à un framework de test puissant qui saura sûrement combler vos attentes et vous permettre de mettre le pied à l’étrier sans difficulté.

Pour vous qui utilisez déjà un autre framework de test, si vous êtes satisfait et que les dépendances ne vous gênent pas, vous ne trouverez rien de novateur. Si par contre, comme moi, vous n’utilisiez ces frameworks uniquement pour les possibilités de BDD et de mocks alors MiniTest saura vous ravir !

L’équipe Synbioz.

Libres d’être ensemble.