Dobře rozjetá webová aplikace bežící pár měsíců na produkci. A žádné testy. Přesně k takovému projektu jsem se dostal a dneska popíšu, jaké změny jsem nutně potřeboval zavést, abych projekt mohl úspěšně a hlavně relativně v klidu rozvíjet.

Integrační testy

Z předchozích zkušeností si myslím, že když přijdete k projektu bez testů, měli byste začít od té nejvyšší úrovně. Zafixujete tak aktuální chování aplikace z pohledu uživatele. Bez toho se jako nový vývojář těžko můžete pustit do nějakých úprav. Také do projektu důkladně proniknete a objevíte spoustu nelogičností a opomenutí (způsobených právě absencí testů). Zároveň je to způsob poměrně nejrychlejší a nejlevnější.

My jsme se rozhodli pro selenium, což není z mnoha důvodů úplně nejlepší a nejoblíbenější nástroj, ale po několika dnech a iteracích máme stabilní testy hlídající všechno důležité. Několik věcí, na které jsme přišli:

  • relativní timeouty
  • před všechny click, setValue apod. metody automaticky přidáváme waitForElementPresent
  • čím kratší test, tím lepší - máme několik souborů namísto několika suites v jednom souboru
  • důležitá je správná kombinace verzí selenia a firefoxu

Máme kolem 60 testů, trvajících okolo 15 minut. A to je hodně, ale k tomu se ještě dostaneme.

Continuous delivery

Jeden artefakt, který si posíláte skrz celou CI pipelinu, skrz všechny prostředí. Ideálně. Naše CI/CD vypadá zhruba následovně:

  1. Push do gitu
  2. CI: Build devového docker image, na kterém následně beží linter a unit testy
  3. CI: Export filesystému z devového image
  4. CI: Build produkčního docker image
  5. CI: Spuštění selenia v dockeru oproti produkčnímu dockeru
  6. CI: Všechno OK? Push do dockerhubu a automatický deploy na stage
  7. Manuální QA a případný tag jakože production ready (přes slack)
  8. Manuální deploy na produkci (přes slack)

Body 2, 3 a 4 ještě rozvedu. Jak devový, tak produkční docker image vychází ze stejného alpine:node, to je důležité! Devový přidává navíc vrstvu, kde se instaluje npm a gulp, což jsou věci, které na produkci nepotřebujete.

I kdyby jste docker nakonec nepoužívali, jenom tím, že se o rozběhnutí aplikace v dockeru pokusíte, tak vaši aplikaci vylepšíte. V našem případě jsme například nahrazovali process.env v klientském javascriptu za klasickou dependency injection, abychom mohli všechno dávat aplikaci z venku skrz env proměnné. Znáte 12 factor apps?

Docker na CircleCI není ideální, ale po několika hodinách zkoušení máme stabilní pipelinu.

Paralelizace testů

Build nyní trvá okolo 22 minut. Dlouho trvá build dockeru (cca 5 minut), s tím ale zatím bohužel nic neuděláme. Druhou nejdelší položkou je exekuce selenia, kterou naštěstí optimalizovat jde.

CircleCI nabízí skvělou vlastnost, a to paralelizaci. Řekněme, že si platíme 3 workery, takže bychom mohli testy rozdělit na třetiny a testy mít hotové za třetinu času!

Jak jsem již zmiňoval, každý test case je ve vlastním souboru. Nejjednodušší řešení paralelizace by mohlo vypadat následovně:

#!/bin/bash

i=0
for testfile in $(find ./test -name "*.js" | sort); do
  if [ $(($i % $CIRCLE_NODE_TOTAL)) -eq $CIRCLE_NODE_INDEX ]
  then
    ./node_modules/.bin/nightwatch --test $testfile
  fi
  ((i=i+1))
done

Problém je, že pro každý test case se znovu spouští nová instance firefoxu, což ve výsledku trvá dost dlouho a nic moc pozorovatelného neušetříme.

Druhou možností je rozhodit testy do adresářů a nightwatch spouštět s --group. To už je lepší a dostačující řešení, které v ideálním případě skutečně stáhne dobu exekuce na třetinu.

Jenže co se stane v případě, že každý test je jinak dlouhý? Co když testy rozhodíme neideálně a jeden worker tak bude mít mnohem větší porci než ti ostatní?

Já jsem to vyřešil tak, že si do $CIRCLE_ARTIFACTS ukládám výsledky nightwatche. Při dalším buildu si vytáhnu poslední úspěšné, rozparsuju je a testy rozkopíruju podle času potřebného na exekuci do stejných (hodně podobných) skupin. Pro více informací koukněte na partition problem nebo na náš společný článek s Ondrou Matějkou, který jsme napsali pro Blueberry.

Nakonec jsme se dostali na nějakých 11-12 minut. Což už docela jde.