La bonne nouvelle, c’est qu’il y a de plus en plus de mecs qui se mettent au BDD (le développement orienté par le comportement), qui est une forme de TDD (le développement orienté par les tests) mais dans lequel on test les fonctionnalités en priorité.

La mauvaise nouvelle, c’est que comme tout mouvement, il vient avec son lot de trucs hype super cool, et notamment cucumber de chez les rubystes. Bah oui, c’est fashion, ça vient forcément de Ruby. Évidement, comme les pythonistes peuvent pas supporter que les codeurs Ruby aient un truc qu’ils aient pas, même si c’est un truc parfaitement inutile, ils se sont dit qu’ils allaient pondre un équivalent : lettuce.

Les deux outils sont aussi inutiles l’un que l’autre, même si ont fait abstraction de leurs noms douteux, car après tout, c’est une marque de fabrique dans notre métier.

Le principe

Vous écrivez un scénario de tests:

Feature: Manipulate strings In order to have some fun As a programming beginner I want to manipulate strings Scenario: Uppercased strings Given I have the string "lettuce leaves" When I put it in upper case Then I see the string is "LETTUCE LEAVES

Vous écrivez des étapes:

>>> from lettuce import * >>> @ step ( 'I have the string "(.*)"' ) ... def have_the_string ( step , string ) : ... world . string = string ... >>> @ step ( 'I put it in upper case' ) ... def put_it_in_upper ( step ) : ... world . string = world. string . upper ( ) ... >>> @ step ( 'I see the string is "(.*)"' ) ... def see_the_string_is ( step , expected ) : ... assert world. string == expected , \ ... "Got %s" % world. string >>> from lettuce import * >>> @step('I have the string "(.*)"') ... def have_the_string(step, string): ... world.string = string ... >>> @step('I put it in upper case') ... def put_it_in_upper(step): ... world.string = world.string.upper() ... >>> @step('I see the string is "(.*)"') ... def see_the_string_is(step, expected): ... assert world.string == expected, \ ... "Got %s" % world.string

Et pour chaque scénar, lettuce va rejouer chaque étape et voir si ça se passe bien. Avantages annoncés par rapport à la manière des pauvres mortels de coder les tests:

c’est élégant;

un non technicien peut comprendre les tests.

La faille

En plus d’être un excellent film dans lequel brille Anthony Hopkins, la faille est le détail qui caractérise la plupart de ces merveilles technologiques, à savoir: on rajoute une surcouche sur un truc que les gens ont déjà du mal à faire.

Concentrez-vous: rassemblez dans votre tête la liste de tous les gens que vous connaissez qui ont jamais écris du code.

Éliminez la majorité qui ne sait même pas ce qu’est un test unittaire.

Dans cette liste, combien écrivent des tests unittaires par eux même ? (je ne le fais pas systématiquement moi-même)

Dans cet échantillon réduit, combien écrivent des tests unittaires AVANT d’écrire le code testé (je le fais rarement moi-même) ?

Dans ce microrésidu d’individus éparse mais dont la volubilité bucolique nous incite à nous poser la question “Pourquoi Dieu a-t-il créé la limule”… Pardon je m’égare.

Donc dans ce qu’il reste, combien il y en a qui écrivent les tests en les orientant comportement en priorité, volontairement ?

Bien.

So… On va écrire tout un outil pour rajouter en travail et en complexité pour faire des tests que déjà il est terriblement difficile de motiver le dev lambda à faire (je n’ai pas dis que c’était une bonne chose, je constate, c’tou).

Parce que “c’est élégant” ? Ahem.

Parce que “un non technicien peut comprendre les tests” ?

Laissez-moi démonter ces assertions avec plaisir.

Les tests, dans la vraie vie vivante (ou VVV)

Évidement, si vous allez sur les sites des deux outils cités. Dans le premier, trouver un tuto clair prend 20 minutes (normal, c’est une communauté Ruby, c’est souvent comme ça). Dans la version Python, leur tuto fait un test sur la fonction factoriel.

Excusez-moi messieurs mais:



1 – si c’est un code aussi simple qu’une fonction factorielle, une doctest règle le problème facilement, sans avoir à apprendre une techno en plus, tout en contribuant à la documentation.

Entre leur exemple qui implique deux fichiers, dont l’un contient ça:

Feature: Compute factorial In order to play with Lettuce As beginners We'll implement factorial Scenario: Factorial of 0 Given I have the number 0 When I compute its factorial Then I see the number 1 Scenario: Factorial of 1 Given I have the number 1 When I compute its factorial Then I see the number 1 Scenario: Factorial of 2 Given I have the number 2 When I compute its factorial Then I see the number 2 Scenario: Factorial of 3 Given I have the number 3 When I compute its factorial Then I see the number 6 Scenario: Factorial of 4 Given I have the number 4 When I compute its factorial Then I see the number 24

Et une version de tests en doctests propre, pythonique, concise et qui ajoute de la doc (faudra d’ailleurs faire un article…):

def factorial ( number ) : """ Calculate the factorial of a number. E.g: >>> factorial(0) 1 >>> factorial(1) 1 >>> factorial(2) 2 >>> factorial(3) 6 >>> factorial(4) 24 """ number = int ( number ) if ( number == 0 ) or ( number == 1 ) : return 1 else : return number*factorial ( number- 1 ) def factorial(number): """ Calculate the factorial of a number. E.g: >>> factorial(0) 1 >>> factorial(1) 1 >>> factorial(2) 2 >>> factorial(3) 6 >>> factorial(4) 24 """ number = int(number) if (number == 0) or (number == 1): return 1 else: return number*factorial(number-1)

J’ai comme un doute sur l’efficacité de leur truc.

J’en profite au passage pour signaler qu’une factorielle en Python, ce serait plutôt ça:

def factorial ( number ) : number = int ( number ) if number in ( 0 , 1 ) : return 1 return number * factorial ( number - 1 ) def factorial(number): number = int(number) if number in (0, 1): return 1 return number * factorial(number - 1)

Mais là je mérite vraiment le tampon drosophilia fucker.

2 – moi dans la vraie vie vivante, mes tests ils ressemblent plutôt à ça:

def test_get_tasks_chain_status ( self ) : states = ( 'PENDING' , 'PROGRESS' , 'SUCCESS' ) try : celery. conf . CELERY_ALWAYS_EAGER = False res = chain ( full_chain. s ( { 'subject_url' : 'http://www.service.com/?id=tPEE9ZwTmy0' , 'subject_id' : 'test' , 'service' : "service_name" } ) , task_1. s ( ) , task_2. s ( ) ) . apply_async ( ) tasks_id_list = get_task_id_list ( res ) tasks = set ( ) while res. state != 'SUCCESS' : task_status = get_tasks_chain_status ( tasks_id_list ) tasks. add ( task_status [ 'name' ] ) self . assertTrue ( task_status [ 'state' ] in states ) task_status = get_tasks_chain_status ( tasks_id_list ) tasks. add ( task_status [ 'name' ] ) self . assertTrue ( task_status [ 'state' ] in states ) self . assertEqual ( tasks , set ( [ "tasks.full_chain" , "tasks.task_1" , "tasks.task_2" ] ) ) finally : celery. conf . CELERY_ALWAYS_EAGER = True # Since we are using a separate celery process for this one with # a different set of settings, we have to remove the generated file # manually try : shutil . rmtree ( os . path . join ( self . OLD_CONTENT_FILE_ROOT , 'test' ) ) except : pass def test_get_tasks_chain_status(self): states = ('PENDING', 'PROGRESS', 'SUCCESS') try: celery.conf.CELERY_ALWAYS_EAGER = False res = chain(full_chain.s({'subject_url': 'http://www.service.com/?id=tPEE9ZwTmy0', 'subject_id': 'test', 'service': "service_name"}), task_1.s(), task_2.s()).apply_async() tasks_id_list = get_task_id_list(res) tasks = set() while res.state != 'SUCCESS': task_status = get_tasks_chain_status(tasks_id_list) tasks.add(task_status['name']) self.assertTrue(task_status['state'] in states) task_status = get_tasks_chain_status(tasks_id_list) tasks.add(task_status['name']) self.assertTrue(task_status['state'] in states) self.assertEqual(tasks, set(["tasks.full_chain", "tasks.task_1", "tasks.task_2"])) finally: celery.conf.CELERY_ALWAYS_EAGER = True # Since we are using a separate celery process for this one with # a different set of settings, we have to remove the generated file # manually try: shutil.rmtree(os.path.join(self.OLD_CONTENT_FILE_ROOT, 'test')) except: pass

On est loin de la factorielle: c’est un test de récupération de statu d’une chaîne de tâches asynchrones. Désolé les gars mais on arrête de coder des factorielles à la sortie de la fac.

Allez me décrire ça dans le DSL de lettuce/cucumber. Oh, c’est possible, ça va juste me prendre du temps. Beaucoup de temps. Parce que ce test, j’ai mis une demi-heure à l’écrire tel qu’il est: c’est la résultante de moult essais, sans compter les modifs a posteriori, car le code qu’il teste n’est pas resté en jachère.

Donc j’ai appris à me servir des tests unitaire, incluant les doctests, j’ai rajouté unittests2 et nose à ma boîte à outil, je connais le tests runner de Django, et par dessus, on voudrait rajouter une surcouche, qui me rajoute du travail dans l’écriture de mes tests. Tests déjà bien chiant à faire, j’ai vraiment PAS besoin d’un démotivateur supplémentaire.

Mais z’attendez, si encore le coût se faisait uniquement à l’écriture du test…

Ces libs, c’est une doc à lire, puis du temps sur leurs fora, et une transposition du concept pour des vrais tests et pas des fonctions playmobiles. Plus le débuggage, plus la dépendance, plus le fait que toute personne qui passe derrière moi devra perdre le même temps.

Tout ça parce que “c’est élégant” ?

Nan, le code est un langage naturel pour moi, le langage ordinaire pour écrire du code n’est pas naturel pour moi. Je suis un dev, je lis du Python au pti dej, c’est Python qui est naturel pour moi, pas un DSL boiteux qui me limite dans ce que je peux écrire histoire de se la toucher en ressemblant à un haïku. Ce n’est pas élégant.

Alors tout ça parceque “un non technicien peut comprendre les tests” ?

Vous croyez qu’une quelconque explication va permettre à une personne lambda de comprendre l’essence du test test_get_tasks_chain_status ? Je ne parle même pas de l’écrire, hein (ce que suggère la doc des outils).

Parce que si c’est juste pour que le stagiaire regarde un tableau et soit capable de citer le nom du test qui a chié, un serveur Jenkins fait très bien l’affaire, merci.

Donc, chers développeurs qui bossez sur ces outils, vous êtes brillant, compétant et vous avez la foi de faire ce faire un dev si énorme. Par pitié, transposez ce potentiel sur des trucs utiles pour la majorité des gens comme une version asynchrone de Django qui puisse concurrencer NodeJs et qui soit plus facile que Twisted, une interface Git humainement acceptable, l’édition de code collaborative qui marche out of the box pour Sublime Text, un blog qui mélange shaarli + status net + wiki + wordpress en une seule interface, un lecteur de flux RSS destop qui ait un historique des articles lus et une recherche qui marche, un jeu video de course de bagnoles qui fasse s’affronter la batmobile, la doloréanne et Bumbo, un film sur le voyage dans le temps qui ait un scénario cohérent, un téléphone non smartphone avec un lecteur mp3 décent et un clavier AZERTY, la paix dans le monde et la disparition des black mambas qui décidément ne servent vraiment à rien.

Merci