TDD C'est quoi ? (En ruby bien sur !)

Publié par Yannick Francois Dim 28 sept 2008 17:14:00 GMT

Voici un petit billet d’initiation au Développement piloté par les Test (dit TDD pour Test Driven Development) avec Ruby. Initialiement publié sur le site de l’association RubyFrance

Imaginons que nous ayons besoin d’un petit objet nous permettant d’afficher un nom. En bon développeur, nous allons d’abord écrire notre test.

require "test/unit"

class TestMonObjet < Test::Unit::TestCase
  def test_attribut
    monObjet = MonObjet.new("un titre")
    assert_equal "un titre", monObjet.nom
  end
end

Executons le test:

rubyFrance:~ $ ruby testMonObjet.rb
Loaded suite testMonObjet
Started
E
Finished in 0.001079 seconds.

  1) Error:
test_attribut(MonObjetTest):
NameError: uninitialized constant MonObjetTest::MonObjet
    testMonObjet.rb:6:in `test_attribut'

1 tests, 0 assertions, 0 failures, 1 errors

Mince une erreur. Vous allez me dire, c’était couru d’avance, on a encore rien codé. Bien. Allons-y alors. D’abord nous allons ajouter le fichier contenant l’objet que nous allons créer.

require "monobjet.rb"

Ensuite créons ce fichier:

rubyFrance:~ $ cat monobjet.rb
class MonObjet
end

Cela suffira largement pour empecher l’erreur précedente. C’est un point important dans l’univers TDD. Il ne faut rien faire de plus que ce que les tests nous demande. Cela rejoint également un autre concept: YAGNI (You Ain’t Gonna Need It).

Executons encore ce test:

rubyFrance:~ $ ruby testMonObjet.rb
Loaded suite testMonObjet
Started
E
Finished in 0.00193 seconds.

  1) Error:
test_attribut(MonObjetTest):
ArgumentError: wrong number of arguments (1 for 0)
    testMonObjet.rb:7:in `initialize'
    testMonObjet.rb:7:in `new'
    testMonObjet.rb:7:in `test_attribut'

1 tests, 0 assertions, 0 failures, 1 errors

Hmm, encore une erreur, mais cette fois ce sont les paramètres de notre objet qui pose problème. Bien, corrigeons notre objet.

rubyFrance:~ $ cat monobjet.rb
class MonObjet
  def initialize un_nom
  end
end

Executons encore ce test:

rubyFrance:~ $ ruby testMonObjet.rb  
Loaded suite testMonObjet
Started
E
Finished in 0.001091 seconds.

  1) Error:
test_attribut(MonObjetTest):
NoMethodError: undefined method `nom' for #<MonObjet:0x80e4d100>
    testMonObjet.rb:8:in `test_attribut'

1 tests, 0 assertions, 0 failures, 1 errors

Encore une erreur. Mais cette fois c’est la method nom qui est manquante pour MonObjet. Ajoutons la:

rubyFrance:~ $ cat monobjet.rb
class MonObjet
  def initialize un_nom
  end
  def nom
  end
end

Executons le test (oui, en tdd, on passe notre temps à tester ! :-)):

rubyFrance:~ $ ruby testMonObjet.rb  
Loaded suite testMonObjet
Started
F
Finished in 0.198677 seconds.

  1) Failure:
test_attribut(MonObjetTest) [testMonObjet.rb:8]:
<"un titre"> expected but was
<nil>.

1 tests, 1 assertions, 1 failures, 0 errors

Voilà qui deviens interessant. Cette fois, ce n’est pas une erreur, c’est un echec du test. La méthode “nom” ne renvoi pas la bonne valeur. La situation d’echec dans le test unitaire est aussi appelé “la barre rouge”. Et quand il y a une barre rouge, le principe est de la faire redevenir verte le plus rapidement possible (en ajoutant très peu de code voir en enlevant du code).

Modifions donc rapidement notre code pour répondre au besoin du test:

rubyFrance:~ $ cat monobjet.rb
class MonObjet
  def initialize un_nom
  end
  def nom
    "un titre"
  end
end

Doucement, doucement, je vous vois venir, oui j’ai mis une valeur en dur, executons le test (c’est barre rouge), nous en parlons juste après.

rubyFrance:~ $ ruby testMonObjet.rb  
Loaded suite testMonObjet
Started
.
Finished in 0.000902 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

Voilà, le test passe. Nous pouvons maintenant parler. J’ai mis une valeur en dur dans la méthode “nom”, cela vous dérange ? Et bien pas moi. Je répond ici au besoin exprimé dans le test. Mais je n’ai pas dit que nous allions nous arreter là ! Ajoutons un test pour bien préciser notre besoin.

require "test/unit"

class TestMonObjet < Test::Unit::TestCase
  def test_attribut
    monObjet = MonObjet.new("un titre")
    assert_equal "un titre", monObjet.nom
  end
  def test_attribut_autre
    monObjet = MonObjet.new("un autre titre")
    assert_equal "un autre titre", monObjet.nom
  end
end

Executons le test unitaire maintenant enrichi d’un test.

rubyFrance:~ $ ruby testMonObjet.rb  
Loaded suite testMonObjet
Started
.F
Finished in 0.024359 seconds.

  1) Failure:
test_attribut_autre(MonObjetTest) [testMonObjet.rb:12]:
<"un autre titre"> expected but was
<"un titre">.

2 tests, 2 assertions, 1 failures, 0 errors

Forcement, avec une valeur en dur, cela ne vas pas. Faisons passer la barre au vert avant de discuter:

rubyFrance:~ $ cat monobjet.rb
class MonObjet
  def initialize un_nom
    @nom = un_nom
  end
  def nom
    @nom
  end
end

Executons le test:

rubyFrance:~ $ ruby testMonObjet.rb  
Loaded suite testMonObjet
Started
..
Finished in 0.001734 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

Parfait ! Barre verte !

Bien, maintenant, on peut laisser étaler nos connaissance en ruby pour effectuer un petit refactoring:

rubyFrance:~ $ cat monobjet.rb
class MonObjet
  attr_reader :nom
  def initialize un_nom
    @nom = un_nom
  end
end

Executons le test a nouveau pour être sur que ce refactoring n’a pas changé la donne:

rubyFrance:~ $ ruby testMonObjet.rb  
Loaded suite testMonObjet
Started
..
Finished in 0.001734 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

Un des interêt de faire un développement piloté par les tests c’est de tendre une sorte de filet de sécurité permettant de donnée plus de courage, ou au moins de tranquillité pour effectuer le refactoring. Mais il existe bien d’autre avantage à ce mode de développement. Notamment celui de ne pas faire plus que nécessaire.

Les tests ainsi écrit, modifié, mis à jour permette de disposer à tout moment d’une documentation sur l’execution du programme.

Des shoes propres avec Ajax

Publié par Yannick Francois Sam 30 août 2008 12:57:00 GMT

Voilà que Shoes se pare d’une nouvelle fonctionnalitée: xmlHttpRequest. En gros, et pour reprendre sont exemple, vous pouvez lancer une opération de download tout en continuant à travailler…

Je vous laisse voir cela plus en détail sur le blog d’hackety.org: threadedDownloadsInShoes

Merci qui ? Merci _Why !

JRuby 1.1.4

Publié par Yannick Francois Ven 29 août 2008 07:03:00 GMT

La nouvelle version de JRuby viens de sortir: Jruby 1.1.4 .

Au programme, pas mal de chose dont

  • Un gros refactoring de la couche d’intégratiohn java.
  • de 2 à 20 fois plus rapide sur la plus part des fonctionnalitées
  • Les exceptions java peuvent être maintenant récupérer directement dans Ruby
  • Amélioration de la gestion mémoire
  • Début du support de Ruby 1.9 (vivement les fibres dans jruby ! je veux voir ça)
  • Amélioration des performances
  • Pool de Thread amélioré
  • Accès concurent sur les tableaux amélioré
  • Et 72 bug résolu depuis la version 1.1.3

Dans les prochain mois, l’équipe va travailler pour essayer de sortir des release plus fréquement (quelque chose comme une fois par mois). Le but étant de corriger au plus vite les divers problème, et apporté les évolutions plus rapidement aux utilisateurs.

Un bon projet toujours sur un bon rythme :)

Shoes change de pompes 2

Publié par Yannick Francois Mar 19 août 2008 08:07:00 GMT

Le micro framework graphique initié par _Why Shoes a un nouveau site dédié: http://shoooes.net/

Un petit rappel également pour le très intessant The Shoebox qui est une gallerie d’application Shoes.

On en reparlera plus tard ;-)

Ruby : Test Unitaire 5

Publié par Yannick Francois Ven 15 août 2008 12:41:00 GMT

Ruby bénéficie comme beaucoup d’autres langages modernes de son framework de test. Voici donc une petite documentation sur comment écrire un test unitaire pour Ruby.

Pour ceux qui ne savent pas ce qu’est un test unitaire, je vous renvoie sur l’article Test Unitaire [wikipedia]

Pour commencer il nous faut importer la librairie “Test::Unit”

require "test/unit" 

Rien que cela nous permet déjà de faire un premier test en executant notre script.

yannick@libellule:~/Code/RubyFrance/testUnit $ ruby testUnit.rb
Loaded suite testUnit
Started

Finished in 0.000607 seconds.

0 test, 0 assertions, 0 failures, 0 errors

Le fait d’inclure la librairie de test unitaire permet d’avoir un comportement par défaut qui va:

  • Charger la suite de test à executer
  • lancer les tests
  • faire l’affichage des resultats de test
  • faire un compte rendu de cette execution

Ajoutons un test maintenant

require "test/unit" 

class StringTest < Test::Unit::TestCase
  def test_length
    s = "Bon test à tous !" 
    assert_equal(17, s.length)
  end
end

Nous avons défini une classe, celle-ci doit étendre TestCase. Cela permet au framework de test de s’y retrouver. Chaque méthode de test définie ensuite doit contenir test en début de nom (le caractère _ n’est placé que pour une meilleur lisibilité et selon les conventions couramment appliquées en Ruby)

Les méthodes assert (ici assert_equal, mais il en existe beaucoup d’autres) permettent d’effectuer un test. Ici un test d’égalité, mais nous pourrions également vérifié une différence, un bouléen répondant vrai ou faux et d’autres encore (voir la documentation sur le module Test::Unit::Assertions).

Après execution, voici le résultat:

yannick@libellule:~/Code/RubyFrance/testUnit $ ruby testUnit.rb
Loaded suite testUnit
Started
.
Finished in 0.000802 seconds.

1 test, 1 assertions, 0 failures, 0 errors

Un test a été executé avec succès.

Ajoutons encore un test pour avancer:

require "test/unit" 

class StringTest < Test::Unit::TestCase
  def test_length
    s = "Bon test à tous !" 
    assert_equal(17, s.length)
  end
  def test_expression_substitution
    assert_equal("", "#{'ah! ' * 3}")
  end
end

Après exécution nous obtenons:

yannick@libellule:~/Code/RubyFrance/testUnit $ ruby testUnit.rb
Loaded suite testUnit
Started
F.
Finished in 0.000827 seconds.

  1) Failure:
test_expression_substitution(StringTest) [testUnit.rb:12]:
<""> expectedbut was
<"ah! ah! ah! ">.
2 test, 2 assertions, 1 failures, 0 errors

Et voilà, comme vous l’aviez deviné, nous avons une erreur. Dans notre cas, l’erreur viens de notre test.

On vois ici l’interêt de mettre chaque test sur un domaine différent dans une méthode différente: on vois facilement quel type de test nous voulions effectuer. Dans le développement d’une application complète, avec plusieurs dizaines d’objets à tester, et plusieurs dizaines de méthodes sur chacun d’eux, les erreurs d’exécution de test peuvent devenir un vrai casse-tête.

Effectuons la correction:

require "test/unit" 

class StringTest < Test::Unit::TestCase
  def test_length
    s = "Bon test à tous !" 
    assert_equal(17, s.length)
  end
  def test_expression_substitution
    assert_equal("ah! ah! ah! ", "#{'ah! ' * 3}")
  end
end

exécution du test:

yannick@libellule:~/Code/RubyFrance/testUnit $ ruby testUnit.rb
Loaded suite testUnit
Started
..
Finished in 0.001273 seconds.

2 test, 2 assertions, 0 failures, 0 errors

Et voilà. Vous devriez être capables de commencer à écrire quelques tests, mais ce n’est qu’un début !.

Cet article a été écrit pour le site de l’association RubyFrance, vous pourrez le retrouver dans les documentations proposées par l’association: RubyFrance: TestUnitaire

Billets précédents: 1 2 3 ... 12