Tests aquatiques đ§
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 :
- 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.
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 utilisantTRUNCATE TABLE
;deletion
supprime les données en utilisantDELETE 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 :
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)
:
Et câest ainsi que la lumiĂšre fut :
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.