Friday, June 26, 2009

Pharo et tests unitaires: utilisation des outils

Voici le premier billet d'une petite trilogie d'introduction sur l'écriture  de tests unitaires dans Pharo. La série abordera les sujets suivants:
  • utilisation des outils de test et debuggage (ce billet)
  • écriture et test d'une Collection
  • utilisation des Traits dans les tests unitaires

Pour illustrer mes propos, je prendrai l'exemple extrêmement original de la manipulation d'une liste de films. Je veux créer des films, les ajouter à une liste et connaître le nombre total de films

Création du package Movies:


Pour commencer, créons le package Movies dans lequel nous mettrons toutes nos classes.  Ouvrez le Class Browser, clic-droit dans la première colonne et sélectionnez create package. Saisissez Movies comme nom puis valider.

Le package Movies apparaît dans la liste classée par ordre alphabétique. Comme nous allons l'utiliser souvent, plaçons-le en tête de liste pour le retrouver facilement. Clic-droit sur le package et sélectionnez place package on top.


Notre premier test unitaire:

Passons ensuite à la classe Movie. Un film ayant un titre, nous allons vérifier qu'une instance de Movie possède un accesseur title qui retourne la chaîne passée à la construction de l'instance.

En développement piloté par les tests, on commence par écrire ... le test.

Déclarons une classe MovieTest qui hérite TestCase (classe de base pour les tests unitaires). Dans le panneau d'édition du Class Browser, saisissez le code suivant:

TestCase subclass: #MovieTest
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Movies'

Pour notre test, nous créons deux instances de la classe Movie en spécifiant leurs titres respectifs. Nous vérifions que les titres ont bien été assignés en utilisant la méthode assert de TestCase. Ajoutons la procédure testMovieHasATitle comme ceci:

testMovieHasATitle
    | starWars bladeRunner |
    starWars:= Movie newWithTitle: 'Star Wars'.
    bladeRunner:= Movie newWithTitle: 'Blade Runner'.

    self assert: starWars title = 'Star Wars'.
    self assert: bladeRunner title = 'Blade Runner'


Lors de la sauvegarde de la procédure, Pharo détecte que la classe Movie utilisée n'existe pas et nous propose de la déclarer. Ne nous en privons pas.

Pharo demande de saisir la définition de la classe et propose Object subclass: #Movie, ce qui conviens dans notre cas. De même pour la confirmation d'ajout de la classe dans le package Movie.

Lançons maintenant notre test unitaire. Clic droit sur la classe MovieTest et sélectionnez run the tests (si vous préférez les raccourcis clavier, tapez Ctl+t).

 
Pharo lance le test, détecte une erreur et nous propose d'ouvrir le debugger, ce que nous allons faire.

Implémentation de la classe Movie:
 
Le debugger nous présente la pile d'appel qui a conduit à l'erreur.  Comme par hasard ;), la pile d'appel contient notre méthode MovieTest#testMovieHasATitle. En cliquant dessus, le debugger affiche le détail de la méthode et indique la ligne source de l'exception: on appelle la méthode inexistante newWithTitle de la classe Movie.

Retournons dans le Class Browser pour l'ajouter. Sélectionnez la classe Movie. Etant donné qu'on veut créer une méthode de classe, n'oubliez pas de cliquez sur le bouton class. La méthode doit créer une nouvelle instance de la classe Movie, lui assigner son titre et retourner l'instance.
newWithTitle: aString
    | movie |
    movie := Movie new.
    movie title: aString.
    ^ movie

Une fois la méthode acceptée, retournez dans le debugger et cliquez sur le bouton Proceed pour continuez l'exécution du test là où elle s'était arrêtée (si vous avez fermé le debugger, relancez le test).

Cette fois, le debugger va plus loin et s'arrête dans notre nouvelle méthode Movie#newWithTitle car l'instance de Movie ne connaît pas la méthode title:aString. Ajoutons-là dans le Class Browser (n'oubliez pas de cliquer sur le bouton instance):
title: aString
    title := aString


Lorsque vous acceptez la méthode, Pharo indique qu'il ne connaît pas la variable title. Comme cette variable est un attribut d'instance, sélectionnez declare instance. Si vous cliquez de nouveau sur la classe Movie pour afficher sa définition, vous apercevrez notre nouvelle variable d'instance.

Object subclass: #Movie
    instanceVariableNames: 'title'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Movies'

Continuons l'exécution en cliquant sur Proceed. Nous venons de créer la méthode Movie#title: pour assigner le titre, il nous manque la méthode Movie#title pour le lire. Ajoutez là:
title
    ^ title

Cette fois-ci, victoire !!



Vous remarquerez le voyant vert indiquant que la totalité des tests de notre classe sont passés.

Notez aussi que l'implémentation de la classe Movie a été pilotée par le test: c'est en suivant les erreurs indiquées par le debugger que nous avons écrit le code réel. C'est une des raisons pour considérer le code de test plus important que le code réel.

"OH: the trick is to write unit tests while you're sober, then write the code while you're drunk.", auteur dont j'ai oublié de noter le nom.

Dans le prochain billet, nous écrirons la classe Movies qui recense les films et les tests associés.

Tuesday, June 16, 2009

Jouons avec Pharo et Seaside

J'ai assisté il y a quelque temps a une conférence de Stéphane Ducasse à l'Institut de Management d'Annecy. Co-créateur de projet Pharo et développeur actif de la communauté Smalltalk, Stéphane nous a présenté différents outils dont Seaside, un framework dédié à l'écriture d'applications Web (en insistant sur le terme application).

Pharo est une implémentation Open Source du langage Smalltalk, qui se veut moderne, agile et cohérente. Pharo est né comme un fork de Squeak, puis a évolué en nettoyant l'implémentation des parties obsolètes, bancales ou instables, en simplifiant l'intéraction avec l'environnement de développement et en intégrant des outils de développement efficaces. De plus, chose essentielle, ils ont créé un joli logo.




La première version stable 1.0 ne devrait plus trop se faire attendre (vers l'automne ?)

J'avais déjà assisté a une présentation de Seaside aux RMLL à Mont-de-Marsan. Mais je voulais d'abord découvrir Smalltalk et du coup je suis passé à côté d'un des principaux fonctionnements novateurs qu'apporte Seaside au développements Web.

Stéphane a parcouru un diaporama de Lukas Renggli "The Heretic Web Framework" disponible ici.

Prenons un exemple simple d'application "classique": nous voulons demander deux nombres et afficher le résultat dans une console. En python nous pourrions coder comme suit:

value1 = input("Premier nombre: ")
value2 = input("Second nombre: ")
print(value1+value2)

Tant que l'utilisateur n'a pas saisi de nombre, le programme reste en attente sur la fonction input Nous utilisons le retour de la fonction pour stocker les deux valeurs.

Imaginons la même application en environnement Web. Le programme de calcul fonctionne côté serveur et l'utilisateur se sert de son navigateur Web pour saisir les valeurs. La plupart des systèmes actuels nous propose de créer une vue ou template pour définir l'interface utilisateur, un contrôlleur pour exécuter les requêtes client et renvoyer le résultat. Nous devons généralement implémenter un graphe d'état et manipuler les sessions pour savoir si nous en sommes à la demande de la première ou seconde valeur.

Seaside permet de programmer de la même manière qu'une application classique. L'équivalent Seaside/Smalltalk du code python précèdent est:

| value1 value2 |
value1 := self request: ‘Premier nombre’.
value2 := self request: ‘Second nombre’.
self inform: value1 asNumber + value2 asNumber.

On n'est pas dépaysé....


Maintenant un peu de concret. Téléchargez la machine virtuelle Pharo et l'image intégrant Seaside sur le site du projet.



Sous Linux, on doit malheureusement bricoler un peu. Décompressez la machine virtuelle puis activez le bit d'exécution du binaire squeak:
unzip pharo-vm-0.15.1b-linux.zip
chmod +x pharo-vm-0.15.1b-linux/squeak
On peut ensuite décompresser l'image et la lancer:
unzip pharo0.1-10324web09.06.1.zip
pharo-vm-0.15.1b-linux/squeak pharo0.1-10324web09.06.1/pharo0.1-10324web09.06.1.image

L'image ouverte, Seaside tourne sur le port 8080. Vous pouvez le vérifier en pointant votre navigateur Web à l'adresse http://localhost:8080/seaside.



Dans Seaside, les points d'entrée de nos applications doivent être définis. Nous allons créer une nouvelle classe de type WATask, un "contrôlleur" Seaside, et l'assigner à une URL.

Les classes peuvent être définies un utilisant le Class Browser, via le menu World ouvert par un clic sur le bureau Pharo. Dans la première colonne, sélectionnez Unpackaged (tout en bas) pour créer une nouvelle classe sans la placer dans un package particulier. Dans la zone de saisie du bas, saisissez le code suivant pour déclarer la classe Additionneur qui hérite WATask.
WATask subclass: #Additionneur
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Unpackaged'

Ouvrez le menu contextuel de la zone de saisie via un clic droit et sélectionnez accept pour enregistrer la classe.




Ceci fait, notre classe doit se déclarer comme application accessible directement via un point d'entrée. Pour trouver ce type d'application, Seaside parcours toutes les classes héritant WAComponent (qu'hérite WATask et donc Additionneur) et sélectionne celles dont le retour de la méthode canBeRoot est true.

(Mise à jour: d'autres moyens sont disponibles, voir le commentaire de Damien)

Pour ajouter une méthode de classe, sélectionnez la classe Additionneur dans la seconde colonne puis cliquez sur le bouton class pour visualiser les méthodes de classe. Sélectionnez la catégorie -- all -- dans la troisième colonne, puis entrez le code suivant dans la zone de saisie:
canBeRoot 
    ^true
Enregistrez alors la méthode via la commande accept du menu contextuel.

Vérifions que notre Additionneur peut être accessible depuis l'interface de configuration de Seaside. Ouvrez la page en question depuis votre navigateur Web à l'adresse suivante: http://localhost:8080/seaside/config. Le compte par défaut est admin / seaside.

Dans la section Settings, Add an entry point, saisissez le nom de votre nouveau point d'entrée, par exemple additionneur. Je précise que ce nom est indépendant du nom de la classe (au cas où vous seriez un développeur Rails ;) ). Sélectionnez Application comme Type puis cliquez sur Add.



Vous entrez alors dans la page de configuration de l'application. Sous la section Configuration/General, modifiez le paramètre Root Component pour sélectionnez la classe Additionneur dans la liste déroulante. Cliquez enfin sur Save en bas de la page.

En cliquant sur Close vous revenez à la page principale de configuration de Seaside.

Vous devriez voir apparaître le point d'entrée additionneur associé à la classe Additionneur dans la liste /seaside/.


Si vous cliquez sur le lien maintenant, vous aurez une erreur car nous n'avons pas encore implémenté le fonctionnement de notre application, ce que nous allons écrire de suite.


Retournez dans Pharo. Seaside cherche à évaluer la méthode go d'une instance de notre classe Additionneur. Pour l'ajouter, sélectionnez d'abord les méthodes d'instance en cliquant sur le bouton instance en dessous de la seconde colonne du Class Browser. Cliquez ensuite sur la catégorie -- all -- et saisissez le code présenté précédemment:
| value1 value2 |
value1 := self request: ‘Premier nombre’.
value2 := self request: ‘Second nombre’.
self inform: value1 asNumber + value2 asNumber.
puis enregistrez-la (clic droit, accept).




Maintenant pointez votre navigateur sur l'application à l'adresse suivante: http://localhost:8080/seaside/additionneur et goûtez à la joie d'avoir une application qui fonctionne du premier coup.




Si ce n'est pas le cas, je décline toute responsabilité quant aux dommages irréversibles que pourraient causer mes indications sur la santé de votre ordinateur...

Friday, June 5, 2009

Pages du matin

Dans "Pragmatic Thinking and Learning", Andy Hunt expose plusieurs techniques pour simuler notre prodution d'idées et ne pas les perdre. L'écriture revets une impordance primordiale, et A.Hunt nous conseille de garder constamment à portée de main de quoi écrire - que ce soit un PDA, un carnet ou tout autre support. On retrouve des approches similaires dans "Mind Performance Hacks" de Ron Hale-Hevans (tip 13: Catch Your Ideas)

Pour arriver à imprimer notre créativité sur papier, A.Hunt nous présente la technique des Morning Pages. Cette technique s'appuie sur le constat que notre cerveau droit (le plus créatif, voir mon billet précédent "Pomodoro technique et cerveau droit") bouillone d'activité la nuit. Nous rêvons, pensons et génêrons des hypothèses, situations improbables, du moins pour notre cerveau gauche. Lorsque nous nous levons, notre cerveau droit laisse peu à peu le cerveau gauche prendre la majorité de nos pensées.


Les Morning Pages sont décrites comme un moyen de capter cette effervescence de notre cerveau droit. Le principe s'avère simple. Tous les matins, la première chose à effectuer (avant même de préparer le café, réveiller les enfants, ...) est de s'asseoir à table, prendre un stylo et écrire trois pages. On ne doit pas réfléchir à ce qu'on écrit, ne pas tenter de diriger ses idées ou essayer de formuler ses phrases. Ecrivez vos pensées telles qu'elles arrivent, même si cela semble stupide, illisible, ...

Vous verrez que progressivement se dessinent des choses auxquelles vous n'avez jamais pensé, relatives au travail ou à votre quotidien. Des idées de développement de logiciels, plans de production, organisation d'un week-end en famille, ...

De plus, je trouve que cette manière d'écrire très agréable, rapide et nous donne l'impression d'avoir accomplit quelque chose dès le matin. Si vous voulez avoir un fou rire en soirée, il suffit parfois de relire vos Morning Pages de quelques jours passés, on s'étonne soi-même ;)

Ceci dit, la régularité m'apparaît plus importante que la quantité. On peut faire une analogie avec la musique; il est préférable de travailler son instrument 20 mn tous les jours que 5 heures seulement le dimanche. J'ai trouvé assez difficile d'écrire trois pages régulièrement tout de suite. J'ai finalement commencé par faire une page, puis ma rapidité d'écriture s'améliorant, la quantité progresse naturellement.

Monday, June 1, 2009

Ruby et bases MS Access: Olé !!

J'ai eu besoin d'automatiser quelques manipulations de bases de données MS-Access. Une possibilité en Ruby est d'utiliser les ActiveX ADO grâce au module win32ole. Je vais montrer l'utilisation de ce module avant d'attaquer MS Access


Voici un exemple de pilotage d'Internet Explorer via OLE. Commençons par lancer irb et importer le module win32ole:

C:\sandbox>irb
irb(main):001:0> require "win32ole"
=> true

Créons une nouvelle instance d'Internet Explorer:


ie = WIN32OLE.new("InternetExplorer.Application")

Pour connaître la liste des attributs d'un objet OLE, on utilise WIN32OLE::ole_get_methods:

irb(main):002:0> ie.Application.ole_get_methods
=> [Application, Parent, Container, Document, TopLevelContainer, Type, Left, Top, 
Width, Height, LocationName, LocationURL, Busy, Name, HWND, FullName, Path, 
Visible, StatusBar, StatusText, ToolBar, MenuBar, FullScreen, ReadyState, Offline,
Silent, RegisterAsBrowser, RegisterAsDropTarget, TheaterMode, AddressBar, 
Resizable]

Affichons la fenêtre et allons sur un site au hasard :)

irb(main):003:0> ie.Visible=true
=> true
irb(main):004:0> ie.Navigate("http://magaloma.blogspot.com")
=> nil

Fini de jouer:

irb(main):005:0> ie.Quit
=> nil

Pour se connecter à une base MS Access, nous utilisons l'ActiveX ADODB.Connection. Par exemple:

dbcon = WIN32OLE.new("ADODB.Connection")
dbcon.Open("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=bibliotheque.mdb")

ADOX.Catalog perment d'inspecter la base:

catalog = WIN32OLE.new('ADOX.Catalog')
catalog.ActiveConnection = dbcon
catalog.Tables.count
=> 11
catalog.Tables.Item(0).Name
=> "auteurs"

On peut ensuite récupérer quelques enregistrements avec ADODB.Recordset:

recordset = WIN32OLE.new('ADODB.Recordset')
recordset.Open("SELECT * FROM livres", dbcon)

Pour ressortir les noms des champs:

recordset.Fields.each {|f| puts f.Name}

Ce qui nous retourne:

id
titre
autheur_id

GetRows permet de récupérer les données:

recordset.GetRows.transpose
=> [[1, "Pragmatic Thinking And Learning", 1], [2, "Art Of Unix Programming", 2]
, [3, "Practices Of An Agile Developper", 1]]

Voilà. Merci au site Ruby On Windows pour m'avoir bien aidé.