Tisíce integračních testů do 30 sekund? Ano, jde to.

29. 3. 2023 22:40 Jan Novotný

Rychlá sada testů je klíčovým předpokladem, který motivuje vývojáře k psaní dalších testů a častému spouštění testovací sady. Ideálně by sada testů měla být dokončená v řádu sekund nebo nízkých jednotek minut.

Tento požadavek lze snadno splnit pomocí čistých jednotkových testů, které nemají žádnou interakci s prostředím. Pokud testy zahrnují komunikaci s externím systémem, například s databází, je často nemožné tento předpoklad dodržet. Než budete číst dál, položte si otázku: Jak dlouho běží vaše integrační testy?

Sada testů evitaDB (aktuálně něco málo přes 2700 testů), včetně integračních, běží na vývojářských noteboocích se 6 fyzickými CPU (12 vláken) přibližně 30 sekund. Procesor na mém vývojářském počítači je Intel® Core™ i7–10750H @ 2,60 GHz. Testy využívají všech 6 procesorů a vytvářejí 65 instancí databází, přičemž 13 jich ve špičce běží paralelně vedle sebe, vkládají do databází téměř 9 000 entit (asi 50 MB, 100 tisíc záznamů), přistupují k webovému rozhraní API na 31 portech, generují certifikáty SSL s vlastním podpisem a testují paralelně z klientů HTTP přes šifrovaný protokol. Pokud mi nevěříte, podívejte se na video se záznamem spuštění testů: https://youtu.be/gGgb49kYUHA

Jak jsme dosáhli takových výsledků?

EvitaDB má dvě hlavní výhody:

  1. je to in-memory databáze, která je z podstaty poměrně rychlá
  2. je to lehká „embedovaná“ databáze, kterou lze spustit lusknutím prstu

Ani jedna z výhod však sama o sobě nezpůsobí výsledek, který vidíte na videu.

Klíčovým aspektem je použití podpor v knihovně JUnit 5, která nám umožňuje spouštět naše testy na všech procesorech hostitelského stroje. Vzhledem k tomu, že evitaDB je hlavně in-memory databáze, využíváme procesory téměř naplno a I/O nás moc nebrzdí. Pokud testy běží pouze v jednom vlákně, trvá jejich provedení přibližně 90 sekund.

Zapnutí paralelních testů je otázkou několika řádků v junit-platform. Obtížnější je implementovat testy tak, aby mohly běžet paralelně.

Principy rychlých paralelních, integračních testů

Ve všech integračních sadách, bez ohledu na použitou technologii nebo databázi, existují přirozené bariéry, které je třeba překonat, a zásady, jimiž je třeba se řídit:

Neměnná sdílená data a izolované zápisy

Aby bylo integrační testování rychlé, je třeba předcházet tomu, aby si každá testovací třída (nebo ještě hůře každá testovací metoda) vytvářela vlastní sadu testovacích dat, se kterou dále pracuje. To znamená, že je třeba sestavit obsah testovací datové sady tak, aby splňovala požadavky co největšího počtu testů. Znamená to také, že žádný test nemůže tato sdílená data modifikovat, pokud tak nečiní způsobem, který neovlivní ostatní testy.

Více živých datových sad

První zásadu je ve větším týmu těžké dodržet. Už i v jednočlenném týmu je těžké ji dodržet, protože kód, který píšete, se postupně vyvíjí a rozšiřuje. Nové funkce vyžadují jiné složení datové sady a vy nechcete přepisovat starší testy. Vytvoření další datové sady je přirozený a snadný způsob, jak s nízkými náklady otestovat nové funkce systému. Udržet tedy jednu jedinou sdílenou sadu s testovacími daty je velmi obtížné, a proto je nutné se smířit s tím, že testovacích datových sad bude používáno v testech více.

Protože chceme testy spouštět paralelně, nemůžeme snadno řídit pořadí, v jakém se testy provádějí. Může se snadno stát, že nejprve testy vyžadují datovou sadu A, pak datovou sadu B a poté knihovna JUnit opět spustí testy pracující s datovou sadou A.

Potřebujeme být tedy schopni pracovat s více datovými sadami současně, aniž by docházelo ke kolizi jedné s druhou. Můžeme to udělat s běžnou databází? Pravděpodobně ano, ale se značnou režií. Pokud spustíte databázový stroj v Dockeru, můžete dynamicky vytvořit novou instanci kontejneru. Můžete také vytvořit nové databázové schéma ve stejném databázovém stroji a nechat aplikaci používat správné databázové schéma v rámci konkrétní testovací metody. Obě tyto možnosti přináší své vlastní problémy, ať už jde o nároky na hardware, čekání na start externího konejneru (obecně problémy se synchronizací) nebo složitost implementace jako takové.

Kontrola nad bojištěm

Psaní paralelních aplikací je náročné samo o sobě. Je třeba, aby podpora pro psaní testů poskytla jednoduchý a předvídatelný mechanismus pro práci s datovými sadami, aby vývojáři, kteří jej používají, nebyli zmateni, udrželi si kontrolu nad testy a mohli vždy zjistit, proč test selhal, když selhal.

Čím více překážek bude databáze házet souběžnému testování do cesty, tím důmyslnější a složitější konstrukce budete muset vymyslet, abyste je překonali, a tím těžší bude pro vývojáře přijít na příčinu „podivného“ selhání testů.

Jak to řeší testovací sada evitaDB?

Žádné sdílené úložiště

evitaDB ukládá svá data do lokálního souborového systému. Testovací datasety jsou uloženy v dočasné složce operačního systému – každá instance datasetu ve vlastní podsložce s náhodně vygenerovaným názvem. Při spuštění naší testovací sady můžete pozorovat, jak se ve složce /tmp/evita (v případě Linux OS) objevují a mizí různé podsložky.

Stejný princip je aplikován na webový server evitaDB, který generuje certifikační autoritu a další potřené certifikáty. Všechny jsou uloženy v náhodně pojmenované složce, která je izolována od ostatních instancí.

Klient evitaDB, který musí projít ověřením mTLS a stáhnout generický klientský certifikát, ukládá certifikáty do samostatné izolované složky. Bez tohoto principu by některé testy mohly přepisovat obsah datové sady/certifikátu, zatímco jiné paralelně běžící testy je stále používají. V paralelním programování toto konstatování znamená, že to co se „může“ stát se taky téměř s jistotou stane.

Správa portů

Během testů je spuštěno několik instancí evitaDB současně a některé z nich potřebují otevřít porty pro testované webové API. Logicky tedy potřebujeme spravovat seznam síťových portů používaných každou z instancí evitaDB, protože na jednom portu může naslouchat pouze jeden webový server. O tuto logiku se stará třída PortManager, která udržuje seznam portů používaných každou z testovacích datových sad a sleduje ty uvolněné, když je datová sada zničena, aby je mohl znovu použít pro další datavou sadu, která o ně požádá.

Žádné globálně sdílené proměnné aplikace

Žádná část zdrojového kódu evitaDB nemůže používat tzv. singletony nebo zapisovatelná (mutable) statická pole. To platí nejen pro produkční kód, ale také pro celé testovací sady a podpůrný testovací kód. I když to může znít jednoduše, často je to obtížné dodržet, pokud nemáte kontrolu nad všemi použitými knihovnami a frameworky. Skutečnost, že celá naše sada testů je schopna běžet masivně paralelně, je důkazem toho, že veškerá logika evitaDB je řádně zapouzdřena a izolována v rámci instancí tříd.

Další tah je na vás

Dobrou zprávou je, že stejnou podporu testů můžete použít ve vlastních integračních testech s evitaDB. Přečtěte si naši dokumentaci a zopakujte náš přístup ve svých integračních testech. Snižte čas potřebný k provedení sady integračních testů na minimum a užívejte si pohodlí lokálního spuštění všech testů po každé změně kódu aplikace.

Sdílet