Tests aquatiques 💧

AnneSottise
La Fabrique
Published in
5 min readApr 20, 2018
Garder la tĂȘte hors de l’eau.

Chez KissKissBankbank, nous testons notre code Ruby avec cet outil quelque peu connu dans le Ruby game : RSpec. RĂ©cemment, nous avons automatisĂ© ces mĂȘmes tests avec un outil d’intĂ©gration continue, lui aussi plutĂŽt populaire, j’ai nommĂ© CircleCI.

Puis, las des ~40 minutes nĂ©cessaires pour exĂ©cuter tous nos tests (oui, c’est trĂšs long), nous avons dĂ©cidĂ© d’activer la parallĂ©lisation sur CircleCI, ce qui nous a alors permis de diviser le temps d’exĂ©cution de nos tests par autant de containers utilisĂ©s.

Mais c’est Ă©galement ce qui signa le dĂ©but de La Grande Guerre Des Specs.

(â•ŻÂ°â–ĄÂ°ïŒ‰â•Żïž” ┻━┻

En effet, c’est prĂ©cisĂ©ment aprĂšs la mise en place de cette parallĂ©lisation que nos tests se sont mis Ă  Ă©chouer de maniĂšre alĂ©atoire sur CircleCI, mais pas en local.

Comment reproduire un Ă©chec CircleCI en local ?

Sur CircleCI, dans l’encart TEST, au tout dĂ©but du fichier de logs, deux Ă©lĂ©ments vont se rĂ©vĂ©ler trĂšs utiles :

Un output CircleCI classique.
  • Le premier Ă©lĂ©ment Ă  rĂ©cupĂ©rer est la liste des fichiers de tests qui ont Ă©tĂ© exĂ©cutĂ©s dans le container sĂ©lectionnĂ© ;
  • Le deuxiĂšme Ă©lĂ©ment est le numĂ©ro de seed. Ce numĂ©ro permettra Ă  RSpec de rejouer les specs exactement dans le mĂȘme ordre, et donc de reproduire les mĂȘmes erreurs.

Ensuite, en local :

bundle exec rspec --fail-fast 'spec/api/history_api_spec.rb' 'spec/api/project_api_spec.rb' [...] --seed 35366

(L’option --fail_fast permet de stopper l’exĂ©cution des tests dĂšs le premier Ă©chec.)

En utilisant cette technique, nous avons pu reproduire exactement le mĂȘme Ă©chec en local, et donc identifier la raison de ces noyades alĂ©atoires : les fuites de donnĂ©es.

ÉtanchĂ©itĂ© et impermĂ©abilitĂ©

Le premier side effect de la parallĂ©lisation a Ă©tĂ© de changer l’ordre d’exĂ©cution des tests. C’est en constatant que certains tests Ă©chouaient uniquement s’ils Ă©taient exĂ©cutĂ©s aprĂšs d’autres que nous avons compris que nos tests n’étaient ni impermĂ©ables, ni Ă©tanches.

Under my umbrella∙ah∙ah∙ah, eh∙eh∙eh ☔

Mais-dis moi Jamy, qu’entends-tu donc par tests impermĂ©ables et Ă©tanches ? Eh bien Fred, c’est simple, un test Ă©tanche est un test dont les data ne vont pas “fuir” ; tandis qu’un test impermĂ©able est un test dont le rĂ©sultat n’est pas impactĂ© par les Ă©ventuelles fuites d’autres tests.

C’est Ă©videmment la combinaison des deux qui va ĂȘtre problĂ©matique. Il n’y aura pas de problĂšmes en cas de fuite de donnĂ©es si les tests sont tous impermĂ©ables, comme il n’y en aura pas non plus en cas de tests non impermĂ©ables mais Ă©tanches.

La bonne pratique

Vaut-il donc mieux travailler sur l’étanchĂ©itĂ© ou l’impermĂ©abilitĂ© ?

Une des rĂ©ponses les plus naturelles Ă  cette question serait de dire : Ă©crivons plutĂŽt des tests impermĂ©ables, nous n’aurons alors pas de data Ă  nettoyer et gagnerons en performance.

Le problĂšme avec cette option est qu’elle est tout simplement difficile Ă  mettre en place au niveau de la globalitĂ© d’une Ă©quipe. On ne va pas se mentir, ça n’est dĂ©jĂ  pas simple d’assurer une couverture de test optimale ; il faut parfois insister et argumenter pour que des tests soient tout simplement Ă©crits. Devoir en plus s’assurer que chaque test Ă©crit soit 100% impermĂ©able est une difficultĂ© supplĂ©mentaire. Cela demande plus d’effort du cĂŽtĂ© de celui∙celle qui Ă©crit le test, et plus de vigilance du cĂŽtĂ© des relecteurs∙rices.

Alors que, du cĂŽtĂ© de la team Ă©tanchĂ©itĂ©, on s’affranchit purement et simplement de cette difficultĂ©. On met en place une fois un outil de nettoyage de data et hop, terminĂ© bonsoir ✌

Nous avons donc choisi de travailler sur l’étanchĂ©itĂ©, tout simplement parce qu’il n’était pas possible en terme de planning de repasser sur chacun de nos tests pour s’assurer de leur impermĂ©abilitĂ©, et les rĂ©Ă©crire le cas Ă©chĂ©ant.

Rendre vos tests Ă©tanches

Cette dĂ©cision prise, nous avons alors mis en place DatabaseCleaner, qui, comme son nom l’indique, est un outil de nettoyage de base de donnĂ©es.

Celui-ci propose plusieurs stratégies de nettoyage :

  • truncation supprime les donnĂ©es en utilisant TRUNCATE TABLE ;
  • deletion supprime les donnĂ©es en utilisant DELETE FROM ;
  • transaction annule les transactions.

Il est gĂ©nĂ©ralement conseillĂ© d’utiliser transaction, puisque c’est souvent bien plus rapide de rollback une transaction plutĂŽt que de supprimer des donnĂ©es Ă  postĂ©riori.

Nous avons donc mis en place cette configuration :

database_cleaner.rb

AprĂšs ça, les fuites ont beaucoup diminuĂ©, mais n’ont pas disparu pour autant, puisque tous nos tests utilisant la mĂ©thode before(:all)Ă©taient toujours sujets Ă  des fuites intempestives.

before(:all) VS transaction

Dans un bloc de type before(:each), le code est exécuté dans une seule transaction. Avec la stratégie transaction, cette transaction sera rollback par DatabaseCleaner.

Le code dans un before(:all) étant exécuté en dehors de cette transaction, les data éventuellement générées par ce code ne seront donc pas nettoyées par DatabaseCleaner.

L’exĂ©cution de nos tests Ă©tant dĂ©jĂ  trĂšs longue (40 minutes, rappelez-vous 😱), nous avons dĂ©cidĂ© de ne pas changer globalement notre stratĂ©gie de nettoyage, mais plutĂŽt de maniĂšre ponctuelle. À chaque before(:all), son after(:all) :

exemple d’utilisation ponctuelle du after(:all)

Et c’est ainsi que la lumiùre fut :

Coeur sur nos tests !

Pro tip de fin : il existe cette petite option de configuration dans RSpec :

RSpec.configure do |config|
config.order = :random
end

Cette simple ligne permet de demander Ă  RSpec de jouer systĂ©matiquement les exemples et les groupes d’exemples d’un test dans un ordre alĂ©atoire. C’est un bon moyen d’ĂȘtre sĂ»r∙e qu’un test est cloisonnĂ© de maniĂšre interne, et pas seulement par rapport aux autres tests.

--

--

AnneSottise
La Fabrique

Internet ! (ïŸ‰â—•ăƒźâ—•)*:✧ @kissbankers