- Le développement top-down en uad
Cet article fait suite à la conversation que j’ai eue avec Patrick Fratczak sur la manière de faire du TDD.
Voici donc la méthode que j’emploi pour faire du user acceptance testing. L’exemple décrit ici utilise rails, cucumber et rspec, mais cela fonctionne également bien avant Typolight, cucumber et phpspec.
Soit, je développe un site pour un client qui vend des peintures. Une des fonctionnalité est le catalogue de peinture. Pour compliquer un peu cet exemple (et le rendre donc plus proche de la réalité), disons que ce client veut que lorsqu’on visionne la page d’une peinture, on voit le nombre d’autres visiteurs qui la visionne en même temps. Je lui dis d’abord que cette fonctionnalité n’est pas rentable, si on prend en compte son temps/coût de développement et son retour sur investissement. Mon client m’assure que c’est rentable, grâce au principe « le monde attire le monde » : si un visiteur remarque que d’autres personnes regarde la peinture en même temps que lui, il aura plus envie de l’acheter.
Dans la user story du catalogue, on donc trouve le scénario suivant :
Scénario: Voir le détail d'une peinture Lorsque je vais sur la page de la liste des peinture Et que je suis le lien de la première peinture Alors je dois voir sa photo Et je dois voir son titre Et je dois voir son auteur Et je dois voir son prix Et je dois voir le nombre de personnes qui visualisent ce produit actuellement
Une fois que le client a validé ce scénario, j’écris les steps cucumber suivantes :
Then /je dois voir sa photo/ do response.should have_selector( '#paint .picture img[src*="paint-1-full.jpg"]' ) end Then /je dois voir son titre/ do response.should have_selector( '#paint h1.title:contains("Paint 1 test")' ) end Then /je dois voir son auteur/ do response.should have_selector( '#paint .author:contains("Paint 1 author")' ) end Then /je dois voir son prix/ do response.should have_selector( '#paint .price:contains("10€")' ) end Then /je dois voir le nombre de personnes qui visualisent ce produit actuellement/ do response.should have_selector( '#paint .viewer-count:contains("5")' ) end
Cucumber, couplé à webrat, a cet avantage de permettre de faire des tests précis des vues à base de sélecteur CSS3. De nombreuses autres méthodes sont permises, et notament toute une gamme de méthodes qui executent une action tout en la testant, par exemple .click_link( link ) fait une assertion sur l’existence de ce lien, puis le suit effectivement. Pour ma part, j’aime bien être le plus précis possible dans les sélecteurs dans mes steps afin de me permettre de composer d’un trait mes vues ensuite.
Ayant ces steps, je peux maintenant écrire ma vue :
<div id="paint"> <h1 class="title">< %= @paint.title -%></h1> <div class="picture"> <img src="<%= @paint.imageSRC -%/>" alt="Photo de < %= @paint.title -%>" /> </div><!-- .picture --> <div class="author"> < %= @paint.author -%> </div><!-- .author --> <div class="price"> < %= "%.2f" % @paint.price -%>€ </div><!-- .price --> <div class="viewer-count"> < %= @viewer_count -%> </div><!-- .viewer-count --> </div><!-- #paint -->Je sais maintenant que mon controller doit assigner @paint et @viewer_count. J’ai également déjà de bon indices sur ce dont je vais avoir besoin dans mes modèles et ma base de donnée. Je peux maintenant écrire les test de mon controller. Qu’est-ce que ce controller doit faire? Il doit assigner un modèle Paint et calculer le nombre de personnes qui voient actuellement cet article. Réflichissant à cette deuxième fonctionnalité, je me dis que cela doit être une méthode de classe de Visitor, qui vérifiera les journaux de visite pour une page donnée. Je n’ai pas besoin d’en savoir plus pour l’instant.
require 'spec_helper' describe PaintsController do def mock_paint(stubs={}) @mock_paint ||= mock_model(Paint, stubs) end describe "GET show" do it "assigns the requested paint as @paint and the current viewers count as @viewer_count" do Paint.stub!( :find ).with( "1" ).and_return( mock_paint( :title => 'Paint 1 test', :imageSRC => 'paint-1-full.jpg', :author => 'Paint 1 author', :price => '10.0' ) ) Visitor.stub!( :viewer_count ).with( '/paints/show/1' ).and_return( 5 ) get :show, :id => "1" assigns[ :paint ].should equal( mock_paint ) assigns[ :viewer_count ].should equal( 5 ) end end end
Je peux maintenant écrire mon controller.
class PaintsController < ApplicationController def show @paint = Paint.find( params[:id] ) @viewer_count = Visitor.viewer_count( request.request_uri ) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @paint } end end end
Désormais, je sais que j’ai besoin de deux modèles : Paint et Visitor. Je sais également à quoi ils doivent répondre : :title, :imageSRC, :author et :price pour les instances de Paint, :viewer_count pour la classe Visitor. Qu’est-ce qui sera attribut et qu’est-ce qui sera méthode dans tout cela? Je réalise maintenant que tous ce à quoi doit répondre une instance de paint est un attribut simple. Il s’agira donc d’un modèle simple avec title, imageSRC, author et price en tant que champs de la table `paints`.
À ce stade, je peux utiliser le generateur rspec_model pour créer ce modèle simple et ses tests :
./script/generate rspec_model Paint title:string imageSRC:string author:string price:floatVisitor est un modèle moins simple. Que sais-je sur lui? D’abord, il doit répondre à la méthode :viewer_count. J’écris donc tout de suite ce test.
require 'spec_helper' describe Visitor do it "should give the viewer count" do Visitor.viewer_count( '/paints/show/1' ).should equal( 5 ) end end
Je m’interroge maintenant sur comment retrouver ce nombre. Les visites doivent être loggée dans une table. Un visiteur est considéré comment visitant une page s’il l’a visité dans les 5 précédentes minutes. J’identifie un visiteur par son IP. Je dois effacer les anciennes visites pour ne pas surcharger ma db. Ok, j’ai ce qu’il faut pour écrire mes specs.
D’abord, je sais que chaque visite doit être loggé. Cela passera donc par un before_filter dans application_controller.rb . Je modifie donc mon fichier de test de mon controller ainsi :
require 'spec_helper' describe PaintsController do def mock_paint(stubs={}) @mock_paint ||= mock_model(Paint, stubs) end before( :each ) do Visitor.stub!( :log ) end describe "GET show" do it "assigns the requested paint as @paint and the current viewers count as @viewer_count" do Paint.stub!( :find ).with( "1" ).and_return( mock_paint( :title => 'Paint 1 test', :imageSRC => 'paint-1-full.jpg', :author => 'Paint 1 author', :price => '10.0' ) ) Visitor.stub!( :viewer_count ).with( 'paints/show/1' ).and_return( 5 ) get :show, :id => "1" assigns[ :paint ].should equal( mock_paint ) assigns[ :viewer_count ].should equal( 5 ) end end end
Je peux maintenant écrire le before_filter :
class ApplicationController < ActionController::Base before_filter :log_visit helper :all # include all helpers, all the time protect_from_forgery # See ActionController::RequestForgeryProtection for details def log_visit Visitor.log( request.request_uri, request.ip, Time.now ) end end
Cela m’en dit plus sur mon modèle Visitor. Il possède une méthode de classe :log. Celle ci enregistrera l’ip, la page et la date. Cette méthode devra également être responsable du vidage de la table. Cela fait une requête de plus à chaque visite, mais ça maintiendra la table presque vide. Je peux donc écrire son test :
require 'spec_helper' describe Visitor do it "should give the viewer count" do Visitor.viewer_count.should equal( 5 ) end it "should log visits" do Visitor.should_receive( :delete_all ) ip, page, date = '127.0.0.1', '/show/paint/1', Time.now Visitor.should_receive( :create ).with( :page => page, :ip => ip, :date => time ) Visitor.log( page, id, date ) end end
Je sais maintenant que mon modèle ma table visitors possède les champs :page, :ip et :date. Je peux générer le modèle.
./script/generate model Visitor page:string ip:string date:datetimeEt je peux implémenter ma méthode :log
class Visitor < ActiveRecord::Base attr_accessible :page, :ip, :date def self.log( page, ip, date ) Visitor.delete_all( [ "date < ?", 5.minutes.ago.to_s( :db ) ] ) Visitor.create( :page => page, :ip => ip, :date => date ) end end
Je sais également comment retrouver le nombre de visiteurs d’une page. Je complète donc son test :
require 'spec_helper' describe Visitor do it "should give the viewer count" do page = '/paints/show/1' Visitor.should_receive( :all ).with( :conditions => [ "page = ?", page ] ).and_return( [ 1, 2, 3, 4, 5 ] ) Visitor.viewer_count( page ).should equal( 5 ) end it "should log visits" do Visitor.should_receive( :delete_all ) ip, page, date = '127.0.0.1', '/show/paint/1', Time.now Visitor.should_receive( :create ).with( :page => page, :ip => ip, :date => time ) Visitor.log( page, id, date ) end end
Et j’écris la méthode :
class Visitor < ActiveRecord::Base attr_accessible :page, :ip, :date def self.log( page, ip, date ) Visitor.delete_all( [ "date < ?", 5.minutes.ago.to_s( :db ) ] ) Visitor.create( :page => page, :ip => ip, :date => date ) end def self.viewer_count( page ) return Visitor.all( :conditions => [ "page = ?", page ] ).length end end
À ce point, j’ai terminé l’implémentation de mon scénario. Il ne me reste plus qu’à le tester. Les specs doivent déjà passer, Pour faire passer les tests de cucumber, je dois d’abord écrire ma route et inclure mes fixtures (nombreux sont ceux qui détestent les fixtures, aujourd’hui, je les trouve personnellement plus simple d’utilisation dans le cadre de cucumber).
L’immense avantage de cette méthode est de rendre tout évident. C’est un no-brainer total et il n’y a qu’à développer, au sens strict. Plutôt que de construire un outil en tentant d’imaginer ses usages, puis d’adapter les usages en fonction de l’outil créé ( schéma du type : on va des modèles aux vues ), je défini d’abord l’usage en disant ce que mes outils doivent faire, et je les construis en conséquence ( schéma du type : vues vers modèles ).
L’immense inconvénient est que les tests ne sont pas exécutables avant l’arrivée aux modèles. Il est donc important de procéder à très petite itérations. Celle que je viens de prendre pour exemple est déjà presque trop longue (en fait, il est assez probable qu’en situation réelle, j’aurai d’abord fait le :show classique, puis j’aurais rajouté le :view_count).
- Getters et setters en PHP
Venant du monde de ruby, j’ai pris l’habitude de ne pas faire de distinction dans l’utilisation d’un objet entre une méthode et un attribut. En ruby, un attribut se récupère exactement de la même manière qu’une méthode s’appelle.
Lire la suite- Typolight et les modèles : M comme dans VC
Si on me posait la question du problème majeur dans le core de Typolight, je répondrais : l’implémentation incomplète du pattern MVC. La couche modèle est difficilement utilisable telle-quelle et provoque l’abondance de requêtes SQL dans les contrôleurs. Il y a aussi le problème de la présence de HTML dans les contrôleurs, mais cela ne concernera pas ce tutoriel. Nous verrons cette fois comment rendre aux modèles ce qui appartient aux modèles.
- Hadopi 2 : la loi, c’est moi.
Hadopi version 2 a donc été approuvée par le conseil constitutionnel.Pour rappel, ce même conseil avait rejeté une version précédente en faisant appel à l’article 11 de la déclaration des droits de l’homme de 1789, estimant qu’un filtrage du réseau nuisait à la liberté d’expression. En effet, Internet permet de communiquer et empêcher l’accès à Internet revient à nuire à la « libre communication des pensées et des opinions ».
- Mise en ligne de Stick’innov et framework Typolight
Stick’innov est une société de vente de stickers muraux, vous proposant les classiques stickers adhésifs et pochoirs, ainsi qu’un produit nouveau : le sticker magnétique. Celui-ci peut être choisi avec un support en laize magnétique ou en peinture aimantée. Un large catalogue vous permet de rajouter une touche personnelle corespondant aux différentes ambiences de votre logement, pour adoucir votre salon, ajouter une note exotique ou créer la surprise au détour d’une fantaisie.