Toute application qui se respecte finit par avoir des tâches asynchrones. Qu’elles soient exécutées de manière journalière ou en fonction des actions des visiteurs (avec delayed job par exemple), il faut bien tester celles-ci afin d’être certain de leur bon fonctionnement.

De manière native, Rails ne propose rien de particulièrement simple pour tester vos tâches rake. C’est cependant assez simple à mettre en place.
Nous allons ici voir comment le faire avec rspec.

Il faut, avant chacune de vos spec, initialiser Rake avec le document approprié.

before(:each) do
    @rake = Rake::Application.new
    Rake.application = @rake
    load RAILS_ROOT + '/lib/tasks/maintenance.rb'
    Rake::Task.define_task(:environment)
end
after(:each) do
    Rake.application = nil
end

Nous utilisons load et pas include car nous désirons le recharger à chaque tâche. Pas uniquement la première fois.
Il vous faut par ailleurs modifier le chemin vers le fichier par le chemin approprié dans votre application.

Par la suite dans mon spec_helper.rb, je fais une méthode invoke_task, qui permettra d’invoquer la tâche que je désire.

def invoke_task(name)
    @rake.should_not be_nil
    @rake[name].should_not be_nil
    @rake[name].invoke
end

Et nous écrivons notre test.

it 'should just work fine' do
    invoke_task('task:name')
end

Votre tâche est exécutée avec succès. Vous n’avez plus qu’à vérifier si l’action qu’elle doit réaliser l’a correctement été.
Si par exemple, votre tâche doit supprimer toutes les données de session n’ayant pas été mises à jour depuis plus de 20 minutes, l’un des tests pourrait être :

it 'should delete the sessions if they are too old' do
    # Des sessions doivent être ajoutées auparavant.
    Session.find(:all, :conditions => ['created_at <= ?', 20.minutes.ago]).size.should_not eql(0)
    invoke_task('clear_sessions')
    Session.find(:all, :conditions => ['created_at <= ?', 20.minutes.ago]).size.should eql(0)
end

Notre test passe, cool ! Notre tâche fonctionne donc :)

Un petit article rapide. Lorsque vous testez vos applications, l’envoi d’email est toujours embettant. Vous n’avez pas envie de l’email parte vraiment et souhaitez vérifier qu’il pars correctement.

Du coup il faut utiliser un Mock, qui va simuler l’envoi d’email mais n’enverra, en fait, rien.
Et qui vous permettra tout de même de surveiller l’email envoyé et de vérifier qu’il est correct.

Commençons, dans notre vue, par envoyer notre email :
from django.core.mail import send_mail
def envoi(self):
  send_mail(
    'A propos des tests',
    'C\'est fun quand même',
    'from@example.com',
    'to@example.com'
)

Et testons notre méthode :
from django.core import mail
def test_send(self):
  response = self.client.post('/envoi')
  self.failUnlessEqual(response.status_code, 302)
  self.assertEquals(len(mail.outbox), 1)
  self.assertEquals(mail.outbox[0].subject, 'A propos des tests')

Ainsi lorsque nous lançons notre test, en supposant que l’url /envoi soit configurée pour diriger vers notre action envoi, celle-ci sera exécutée, l’email sera envoyé de manière fictive.
Et vous aurez accès à un tableau dans mail.outbox qui contiendra tous les emails envoyés avec leur contenu, leur sujet et les personnes à qui ils ont été envoyés.

A vous, par la suite, d’écrire les tests appropriés au bon fonctionnement de votre application.

Après avoir vu la mise en place de l’environnement de test et des tests unitaires avec Django, voyons maintenant comment mettre en place nos tests fonctionnels.
En écrivant cet article, je considère que vous avez lu l’article précédent et que vous avez donc compris ma méthode de hiérarchisation des tests.

Voici un test d’exemple (j’allais dire « de test ». Mais c’est un chouilla trop répétitif).
from django.test import TestCase
from django.test.client import Client
from projet.pages.models import Pager
class PagesTest(TestCase):
  def setUp(self):
    self.client = Client()
  def test_view_page(self):
    Page(
      name='Conditions d\'utilisation',
      url='conditions',
      content='nothing for now'
    ).save()
    response = self.client.get('/conditions')
    self.failUnlessEqual(response.status_code, 200)
  def test_view_unknown_page(self):
    response = self.client.get('/nothing/')
    self.failUnlessEqual(response.status_code, 404)

Et analysons le code.
Nous commençons par importer les diverse classes nécessaires à ces tests :

  • TestCase, le framework de test
  • Client, le client HTTP de test
  • Page, le modèle qui nous permet d’ajouter des données permettant ici les tests

Puis nous commençons les tests, en initialisant self.client à une nouvelle instance de Client afin de pouvoir faire des requêtes HTTP.
Dans le premier test, nous créons un élément Page puis nous testons que lorsque nous appellons celui-ci, notre page est bien rendue.
Et dans le second, nous vérifions que nous avons une bien une erreur 404 si nous appellons une page qui n’existe pas.

Vous noterez la méthode self.client.get() permettant de faire un appel HTTP avec la méthode du même nom.

Plusieurs méthodes sont ainsi disponibles pour faire les appels HTTP.

  • get(path, data={}, follow=False) – Fait une requête HTTP get
  • post(path, data={}, follow=False) – Fait une requête HTTP post
  • head(path, data={}, follow=False) – Fait une requête HTTP head
  • options(path, data={}, follow=False) – Fait une requête HTTP options
  • put(path, data={}, follow=False) – Fait une requête HTTP put
  • delete(path, data={}, follow=False) – Fait une requête HTTP delete

A cela s’ajoutent deux méthodes permettant d’identifier et de déconnecter l’utilisateur.

  • login(options) – Identifie l’utilisateur et retourne true si celle-ci a réussi.
    Exemple : self.client.login(username='test', password='test')
  • logout() – Déconnecte l’utilisateur auparavant connecté

Lorsque vous faites une requête quelconque, vous pouvez en tester le résultat. Voici les différentes méthodes disponibles.

  • client – Le client de test qui a été utilisé pour faire la requête
  • content – Le contenu de la page rendue
  • context – L’instance de la classe Context qui a été utilisée pour afficher le contenu de la page. Si il y a eu plusieurs templates de rendus, il s’agira d’une liste de contextes triés par ordre de rendu.
  • request – Les données request qui ont servi à faire la requête
  • status_code – Le code HTTP retourné par la page
  • template – L’instance de template qui a été utilisée afin d’afficher le contenu de la page. Si il y en a eu plusieurs, un tableau de templates triés par ordre d’affichage sera retourné.

Du coup vous avez maintenant toutes les clés en main pour commencer à faire vos tests fonctionnels. Y’a plus qu’à coder !

Par défaut, Django inclue deux frameworks de tests différents. Le premier, Doctest, impose de placer ses tests dans une chaine de caractères.
Le second, Unit Tests, est plus conventionnel et requiert de créer une méthode pour chacun de nos tests.
Le premier nous faisant perdre toute colorisation syntaxique, nous allons donc voir uniquement le second dans cet article.

Chacune de vos applications Django peut prendre des tests. Ceux-ci sont exécutés avec la commande manage.py tests
Qui va exécuter, pour chaque application, toutes les méthodes contenues dans le fichier tests.py de votre application.

Bien évidemment, placer tous nos tests dans ce fichier serait illisible.
Créons donc notre architecture de test :
|- application
|– tests.py
|– test
|— functional
|—- my_functional_test.py
|— unit
|—- my_unit_test.py

Nous séparons donc nos tests unitaires et fonctionnels et permettons de créer plusieurs fichiers pour chaque si nous le désirons.
Puis dans tests.py, il faut les inclure ces fichiers.
from project.application.test.functional.my_test import HomepageTests
from project.application.test.unit.my_test import UserTests

Remplissons maintenant nos tests unitaires.
Dans chaque fichier de test, nous devons faire appel au framework approprié en haut de page.
from django.test import TestCase

Puis nous créons la méthode UserTests, importée plus haut et qui contiendra nos tests.
class SimpleTest(TestCase):
  def test_basic_addition(self):
    self.failUnlessEqual(1 + 1, 2)

Nous testons ici que 1 + 1 est bien égal à 2. A vous par la suite de créer vos tests selon vos besoins.

Les méthodes permettant de faire les tests sont les suivantes :

  • setUp() – Méthode appellée pour préparer les fixtures des tests et donc avant ceux-ci. Il est inutile de l’appeller. En revanche, vous pouvez la surcharger.
  • tearDown() – Méthode appellée après que le test ait été exécuté (qu’il ait réussi ou non).
  • assertTrue(expression, message) – Vérifie que l’expression est bien égale à vrai.
  • assertEqual(first, second, message) – Vérifie que la première expression est égale à la seconde
  • failUnlessEqual(first, second, message) – Alias de assertEqual
  • assertNotEqual(first, second, message) – Vérifie que la première expression est différente de la seconde
  • failIfEqual(first, second, message) – Alias de assertNotEqual
  • assertAlmostEqual(first, second, décimales, msg) – Vérifie que les deux valeurs sont approximativement égales en calculant leur différence, puis l’arrondit à son nombre de décimales (défault : 7) et en les comparant à zéro.
  • failUnlessAlmostEqual(first, second, décimales, msg) – Alias de assertAlmostEqual
  • assertNotAlmostEquat(first, second, décimales, msg) – Vérifie que les deux valeurs sont approximativement égales en calculant leur différence, puis l’arrondit à son nombre de décimales (défault : 7) et en les comparant à zéro.
  • failIfAlmostEqual(first, second, décimales, msg) – Alias de assertNotAlmostEqual
  • assertRaises(exception, callable, …) – Vérifie qu’une exception a été levée. Le test passera si l’exception définie est levée et échouera si il s’agit d’une autre ou qu’aucune exception n’est levée
  • failUnlessRaises(exception, callable) – Alias de assertRaises()
  • failIf(expr, msg) – Opposé de failUnless
  • assertFalse(expr, msg) – Alias de failIf
  • fail(msg) – Retourne une erreur

Vous pouvez donc maintenant tester toutes les méthodes de vos modèles de manière efficace.
Vous pouvez donc continuer avec les tests fonctionnels.

Comme tout framework qui se respecte, Symfony possède une interface permettant d’écrire et exécuter des tests unitaires et fonctionnels.
Voyons un petit peu celle-ci.

Dans votre projet, vous avez le dossier test qui contient trois dossiers.

Bootstrap

Ce dossier contient deux fichiers. functional.php et unit.php.
Ils sont à inclure au début de chacun de vos tests et permettent d’instancier l’environnement.

Unit

Passons donc maintenant aux choses sérieuses avec les tests unitaires :)
Voici un exemple de tests :
require_once dirname(__FILE__).'/../bootstrap/unit.php';

$t = new lime_test(2, new lime_output_color());

$t->comment('is the valid() method valid?');
$t->is(Post::my_test(true), true, 'valid() return the argument we give him');
$t->is(Post::my_test(false), false, 'valid() return the argument we give him');

A côté de ce test, nous avons un modèle Post qui contient une méthode my_test, celle-ci étant on ne peut plus basique puisqu’elle retourne la valeur que l’on lui donne.

Pour commencer, vous constatez que en première ligne de nos tests, nous incluons le bootstrap des tests unitaires.

Ensuite nous créons notre nouveau test, en y signalant que nous aurons deux assertions.
$t = new lime_test(2, new lime_output_color());
La méthode comment() permet de placer un commentaire dans l’interface de test, facilitant ainsi le débugguage ensuite.

Puis vient le moment des tests à proprement parler.
$t->is(Post::my_test(true), true, 'valid() return the argument we give him');
Le test échouera si la valeur retournée par Post::my_test n’est pas égale à true.

De nombreuses méthodes sont disponibles en plus de is().

  • comment($msg) – Affiche un commentaire mais n’exécute aucun test.
  • ok($test, $msg) – Affiche le commentaire et vérifie que $test est vrai.
  • is($value1, $value2, $msg) – Affiche le commentaire et vérifie que $value1 est égal à $value2.
  • isnt($value1, $value2, $msg) – Affiche le commentaire et vérifie que $value1 n’est pas égal à $value2.
  • like($string, $regexp, $msg) – Affiche le commentaire et vérifie que $string respecte l’expression régulière $regexp<./li>
  • unlike($string, $regexp, $msg) – Affiche le commentaire et vérifie que $string ne respecte pas l’expression régulière $regexp.
  • cmp_ok($value1, $operator, $value2, $msg) – Compare les deux arguments avec l’opérateur.
  • isa_ok($value, $type, $msg) – Vérifie le type de l’argument donné.
  • isa_ok($object, $class, $msg) – Vérifie que l’objet est bien de la classe mentionnée.
  • can_ok($object, $method, $msg) – Vérifie la disponibilité de la méthode pour un objet ou une classe.
  • is_deeply($array, $array2, $msg) – Vérifie que deux tableaux ont les mêmes valeurs.
  • include_ok($file, $msg) – Vérifie qu’un fichier existe et qu’il est correctement inclu.
  • fail() – Echoue toujours. Utile pour tester des exceptions.
  • pass() – Passe toujours. Utile pour tester des exceptions.
  • skip($msg, $nb_tests) – Compte comme $nb_tests tests. Utile pour les tests conditionnels
  • todo() – Compte comme un test. Utile pour les tests restant à écrire.

Functional

Et les tests fonctionnels. Voici un exemple de test :
include(dirname(__FILE__).'/../../bootstrap/functional.php');
$browser = new sfTestFunctional(new sfBrowser());

$browser->get('/')
  ->info('1 - The culture is appropriately fr')
  ->with('user')
  ->isCulture('fr')

  ->info('2 - The response is 200 and the content has a title')
  ->with('response')
  ->begin()
  ->isStatusCode(200)
  ->checkElement('body', '/<h1>/i')
  ->end();

Comme précédemment, nous commencons par implémenter le bootstrap.
Puis nous instancions un objet sfTestFunctional, qui fera office de navigateur et s’occupera de faire les appels http et de faire nos validations.

Ensuite nous faisons une requête get sur l’url /.
Et nous entrons dans la vue culture, pour vérifier que la langue définie pas défaut est bien le français. Voir internationalisation

Ensuite nous passons dans la vue response, nous vérifions que le code HTTP est bien 200 et que la page contient une balise h1.
Puis nous fermons cette requête.

Il serait bien évidemment possible de faire d’autres requêtes sans réinstancier le navigateur. Il suffit de refaire un $browser->get('/chemin');

Par ailleurs, comme précédemment, de multiples méthodes sont présentes :

  • get($url, $parameters) – Fait une requête get
  • post($url, $parameters) – Fait une requête post
  • call($url, $method, $paramaters) – Permet de faire un appel en précisant sa méthode (utilisé pour les requêtes PUT et DELETE).
  • back() – Retourne à la page précédente dans l’historique
  • forward() – Va à la page suivante dans l’historique
  • reload() – Recharge la page courante
  • click($name, $arguments, $options) – Clique sur un lien ou un bouton
  • select($name) – Sélectionne un bouton radio ou une case à cocher
  • deselect($name) – Désélectionne un bouton radio ou une case à cocher
  • restart() – Redémarre le navigateur (et la session)

Ainsi que d’autres méthodes pour configurer le comportement du navigateur

  • setHttpHeader($header, $value) – Définit une entête HTTP
  • setAuth($username, $password) – Définit l’identification http (basique)
  • setCookie($name, $value, $expire, $path, $domain, $secure, $httpOnly) – Définit un cookie
  • removeCookie($name) – Supprime un cookie
  • clearCookie() – Supprime tous les cookies
  • followRedirect() – Suis la redirection (et retourne une exception si le lien n’en est pas une)

Une fois que la requête est correctement paramétrée, il vous faut définir la vue que vous désirez avoir et exécuter celle-ci.
Il existe cinq vues. Mais nous n’en verrons que deux dans cet article.

request

Cette vue apporte des méthodes permettant de tester la requête.

  • isParameter($key, $value)> – Teste qu’un paramètre est présent et que sa valeur est correcte
  • isFormat($format) – Vérifie le format de la requête
  • isMethod($method) – Vérifie la méthode de la requête
  • hasCookie($key, $exists) – Vérifie que le cookie existe
  • isCookie($key, $value) – Vérifie la valeur du cookie

response

Cette méthode permet de vérifier le contenu renvoyé par la page.

  • checkElement($selector, $value, $options) – Vérifie qu’un sélecteur CSS respecte les critères donnés
  • isHeader($key, $value) – Vérifie la valeur d’une entête HTTP
  • isStatusCode($code) – Vérifie le code HTTP renvoyé
  • isRedirected() – Vérifie qu’il s’agit d’une redirection

Autres vues

Nous avons donc vu les deux vues les plus couramment utilisées. Cependant il en existe trois autres, que nous ne détaillerons pas ici :

Pour finir, comme vous pouvez le constater, tester ses applications est assez aisé avec Symfony. Alors vous n’avez plus aucune raison de ne pas le faire :)

Simian, aka Similarity Analyser est un outil des plus merveilleux. Il analysera vos lignes de code et détectera des potentielles répétitions.

A cela, nous couplons un gem de Jean-Michel Garnier (qui était présent au RubyCamp Lyonnais), Don’t Repeat Yourself.
Et à partir de maintenant, nous pouvons tester que nos applications Ruby et Rails sont DRY !!
J’ai testé et ça fonctionne plutôt du tonnerre.

Installation du plugin :
Si vous n’avez pas encore le catalogue de gems de GitHub, ajoutez-le :
gem sources -a http://gems.github.com
Puis installez le gem.
gem install dmathieu-dont_repeat_yourself
Vous constaterez que je vous fait installer non pas la version de Jean-Michel, mais la mienne. Les explications viennent plus bas.

Une fois que cela est fait, vous pouvez utiliser le plugin de trois manières possibles.

Dans vos tests

Avec Test::Unit

Créez le test suivant :
test "we don't repeat ourself" do
  assert_dry(rails_application.with_netbeans_reporting)
end

Avec RSpec

Créez le test suivant :
describe "Dupplicate lines Report: Don't Repeat Yourself" do
  it { ruby_project(File.dirname(__FILE__) + '/../').
    with_threshold_of_duplicate_lines(4).
    with_netbeans_reporting.
    should follow_the_dry_principle }
end

En ligne de commande

Tapez la commande suivante
dry-report

Ces rapports DRY sont même encapsulables dans Netbeans ou Textmate. Mais je n’ai pas encore penché mon nez là-dedans. Donc je vous laisse regarder ;)

Vous constaterez par ailleurs que je vous invite à installer le fork que j’ai fait du gem et non pas la version originelle.
La raison à cela est que en Rails 2.3, Test::Unit deviens ActiveSupport::TestCase.
Cette modification n’a pas encore été appliquée au gem originel. Mais je suis persuadé que cela ne saurait tarder ;)

J’ai déjà mentionné Selenium dans mon article de test de code avec JQuery.

Nous allons voir aujourd’hui, en introduction à celui-ci, comment tester votre présence sur Google pour les mots clés de votre choix.
Ceci est cependant un cas d’école. Vous ne pouvez tester (simplement) votre positione exacte. Nous vérifions ici uniquement que vous êtes dans les 10 premiers. Mais il faut bien un exemple pour débuter et vous permettre ensuite de tester vous-même votre site.

Commencez par télécharger et installer Selenium IDE, qui est une extension firefox permettant de générer des tests Selenium.
Une fois celui-ci installé, rendez-vous dans le menu « Outils » du navigateur puis cliquez sur « Selenium IDE ». Une fenêtre s’ouvrira alors, vous permettant de créer votre nouveau test.

De multiples commandes sont disponibles. Ici cependant, nous désirons ouvrir google.com, puis entrer une recherche, cliquez sur le bouton et analyser le résultat.

Notre set de commandes sera donc :

Test google.com Selenium

open /
type q dmathieu
clickAndWait btnG
assertTextPresent dmathieu.com

Exécutez votre test et c’est magique. Vous voyez même le test s’exécuter en direct dans votre fenêtre active :)
Maintenant, c’est bien gentil, vous avez votre test. Mais comment l’automatiser ?

Eh bien vous avez le choix. Java, C#, Perl, PHP, Python, Ruby ?
Il n’y a qu’à choisir, installer la librairie du langage.
Puis faites Fichier > Export Test Case as … Et choisissez le langage de votre choix.

Exécutez le script généré. Et si vous avez correctement installé les librairies, votre test devrait s’exécuter.
Vous avez réalisé votre premier test frontend automatique. Vous n’avez maintenant plus qu’à adapter cela à vos tests QUnit (ou tout autre framework) pour automatiser ceux-ci.

Note : Tester votre présence sur Google de cette manière est assez peu productive.
Pour cela, je vous conseille plutôt RefStats.

Je viens de publier un article sur le blog d’O2Sources, sur le thème suivant : « Ecrire du code testable« .
En effet, vous pouvez avoir toute la bonne volonté du monde à vouloir écrire des tests dans votre code, si celui-ci n’est pas écrit de manière à vous permettre de créer des tests de manière aisée, vous n’arriverez à rien.

Cet article donne donc une liste de 10 conseils pour écrire du code testable.

  1. Ne pas mélanger le constructeur avec le code logique
  2. Demandez les choses. N’allez pas les chercher vous même
  3. Ne faites pas tout le travail dans le constructeur
  4. Evitez les singletons et variables globales
  5. Evitez les méthodes statiques
  6. Favoriser les compositions aux héritages
  7. Favoriser le polymorphisme aux conditions
  8. Mélanger des objets de valeur et des objets de service
  9. Ne mélangez pas tout
  10. Pensez deux fois, n’agissez qu’une

Je vous invite donc à lire cet article « Celui qui voulait rendre son code testable« .

 
Fork me on GitHub