V šedesátých letech minulého století se v softwarovém inženýrství považovalo za základní dobrou praxi testování kódu v průběhu jeho psaní. Průkopníci vývoje softwaru v této éře byli zastánci různých úrovní testování; někteří obhajovali „jednotkové“ testování a někteří ne, ale všichni uznávali důležitost testování kódu.

První testy spustitelnosti možná zavedla Margaret Hamiltonová v polovině 60. let v projektu Apollo, kde stála u zrodu typu kontroly spustitelnosti, kterému dnes říkáme „statická analýza kódu“. Nazývala ji „software vyššího řádu“, čímž myslela software, který pracuje spíše proti jinému softwaru než přímo proti problémové doméně. Její software vyššího řádu zkoumal zdrojový kód a hledal vzory, o nichž bylo známo, že vedou k problémům s integrací.

Do roku 1970 lidé na testování spustitelných souborů většinou zapomněli. Jistě, lidé spouštěli aplikace a tu a tam do nich ručně šťouchli, ale dokud kolem nich neshořela budova, měli za to, že kód je „dost dobrý“. Výsledkem je více než 35 let kódu v produkci po celém světě, který je nedostatečně testován a v mnoha případech nefunguje zcela podle záměru nebo způsobem, který by uspokojil zákazníky.

Myšlenka programátorů testovat za chodu se začala vracet od poloviny 90. let, ačkoli až do současnosti to drtivá většina programátorů stále nedělá. Inženýři infrastruktury a správci systémů testují své skripty ještě méně pečlivě než programátoři svůj aplikační kód.

S přechodem do éry, kdy se rychlé nasazení složitých řešení sestávajících z mnoha autonomních komponent stává normou a kdy „cloudové“ infrastruktury vyžadují, abychom spravovali tisíce přicházejících a odcházejících virtuálních počítačů a kontejnerů v měřítku, které nelze zvládnout manuálními metodami, nelze ignorovat význam spustitelného, automatizovaného testování a kontroly v celém procesu vývoje a dodávek; nejen pro programátory aplikací, ale pro všechny, kdo se podílejí na práci v IT.

S nástupem devops (vzájemné propojení vývojových a provozních dovedností, metod a nástrojů) a trendů jako „infrastruktura jako kód“ a „automatizace všech věcí“ se jednotkové testování stalo základní dovedností programátorů, testerů, správců systémů i inženýrů infrastruktury.

V této sérii příspěvků představíme myšlenku shellových skriptů pro unit testy a poté prozkoumáme několik frameworků pro unit testy, které mohou pomoci učinit tento úkol praktickým a udržitelným ve velkém měřítku.

Dalším postupem, který může být pro mnoho inženýrů infrastruktury neznámý, je správa verzí. Později v tomto seriálu se dotkneme systémů správy verzí a pracovních postupů, které používají vývojáři aplikací a které mohou být efektivní a užitečné i pro inženýry infrastruktury.

Skript pro testování

Vivek Gite publikoval ukázkový shellový skript pro sledování využití disku a generování e-mailového upozornění, když určité souborové systémy překročí určitou mez. Jeho článek je zde: https://www.cyberciti.biz/tips/shell-script-to-watch-the-disk-space.html. Použijme jej jako předmět testu.

Počáteční verze jeho skriptu s přidáním volby -P u příkazu df, která zabrání zalamování řádků ve výstupu, jak navrhuje v komentáři Per Lindahl, vypadá takto:

#!/bin/shdf -HP | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{ print  " "  }' | while read output;do usep=$(echo $output | awk '{ print }' | cut -d'%' -f1 ) partition=$(echo $output | awk '{ print  }' ) if ; then echo "Running out of space \"$partition ($usep%)\" on $(hostname) as on $(date)" | mail -s "Alert: Almost out of disk space $usep%" [email protected] fidone

Vivek skript dále zdokonaluje, ale pro účely tohoto příspěvku poslouží tato verze.

Automatické kontroly funkčnosti

Několik zásadních pravidel pro automatické kontroly funkčnosti, ať už kontrolujeme kód aplikace, skript nebo jakýkoli jiný druh softwaru:

  • kontrola musí pokaždé proběhnout identicky, bez nutnosti ručního dolaďování při přípravě na každé spuštění; a
  • výsledek nesmí být zranitelný vůči změnám prostředí provádění nebo dat či jiným faktorům vně testovaného kódu.

Projde, neprojde a dojde k chybě

Můžete poukázat na to, že je možné, že skript vůbec neproběhne. To je normální u jakéhokoli rámce pro jednotkové testy jakéhokoli druhu aplikace. Možné jsou spíše tři výsledky než dva:

  • Testovaný kód vykazuje očekávané chování
  • Testovaný kód běží, ale nevykazuje očekávané chování
  • Testovaný kód neběží

Pro praktické účely je třetí výsledek stejný jako druhý; budeme muset zjistit, co se pokazilo, a opravit to. Obecně tedy o těchto věcech uvažujeme jako o binárních:

Co bychom měli kontrolovat?

V tomto případě nás zajímá, zda se skript bude chovat podle očekávání při různých vstupních hodnotách. Nechceme naše jednotkové kontroly znečišťovat žádným dalším ověřováním nad tento rámec.

Při prohlížení testovaného kódu vidíme, že když využití disku dosáhne prahové hodnoty 90 %, skript zavolá poštu, aby odeslal oznámení správci systému.

V souladu s obecně uznávanou dobrou praxí pro kontroly jednotek chceme definovat samostatné případy pro ověření každého chování, které očekáváme pro každou sadu počátečních podmínek.

Nasadíme-li si klobouk „testera“, vidíme, že se jedná o věc typu okrajové podmínky. Nemusíme jednotlivě ověřovat četná různá procenta využití disku. Potřebujeme pouze zkontrolovat chování na mezních hodnotách. Proto minimální sada případů, která zajistí smysluplné pokrytí, bude:

  • Odesílá e-mail, když využití disku dosáhne prahové hodnoty
  • Neodesílá e-mail, když je využití disku pod prahovou hodnotou

Co bychom neměli kontrolovat?

V souladu s obecně uznávanou dobrou praxí pro izolaci jednotkových testů chceme zajistit, aby každý z našich případů mohl selhat přesně z jednoho důvodu: Očekávané chování nenastane. V praktické míře chceme nastavit naše kontroly tak, aby jiné faktory nezpůsobily selhání případu.

Nemusí být vždy rentabilní (nebo dokonce možné) zaručit, že vnější faktory neovlivní naše automatické kontroly. Jsou případy, kdy nemůžeme vnější prvek kontrolovat nebo kdy by to vyžadovalo více času, úsilí a nákladů, než je hodnota kontroly, a/nebo se jedná o obskurní okrajový případ, u kterého je velmi malá pravděpodobnost výskytu nebo velmi malý dopad, když se vyskytne. Je to záležitost vašeho odborného úsudku. Obecně platí, že se snažte vyhnout vytváření závislostí na faktorech mimo rozsah testovaného kódu.

Nepotřebujeme ověřovat, zda fungují příkazy df, grep, awk, cut a mail. To je pro naše účely mimo rozsah. Za to je zodpovědný ten, kdo tyto nástroje spravuje.

Chceme vědět, zda výstup z příkazu df není zpracován způsobem, který očekáváme od příkazů grep nebo awk. Proto chceme, aby se při našich kontrolách spouštěly skutečné příkazy grep a awk na základě výstupu z příkazu df, který odpovídá záměru každého testovacího případu. To je v rozsahu, protože argumenty příkazového řádku příkazu df jsou součástí skriptu a skript je testovaný kód.

To znamená, že budeme potřebovat falešnou verzi příkazu df, kterou použijeme při našich jednotkových kontrolách. Takové falešné komponentě se často říká mock. Maketa nahrazuje skutečnou komponentu a poskytuje předdefinovaný výstup, který kontrolovaně řídí chování systému, takže můžeme spolehlivě kontrolovat chování testovaného kódu.

Vidíme, že skript odešle e-mailové oznámení, když souborový systém dosáhne prahové úrovně využití. Nechceme, aby naše kontroly jednotek chrlily spoustu zbytečných e-mailů, takže budeme chtít zesměšnit také příkaz mail.

Tento skript je dobrým příkladem pro ilustraci zesměšňování těchto příkazů, protože u příkazu mail to uděláme jinak než u příkazu df.

Zesměšňování příkazu df

Skript je postaven kolem příkazu df. Příslušný řádek ve skriptu je:

df -HP | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{ print  " "  }'

Pokud byste spustili pouze df -HP, bez pipingu do grepu, viděli byste výstup podobný tomuto:

Filesystem Size Used Avail Use% Mounted onudev 492M 0 492M 0% /devtmpfs 103M 6.0M 97M 6% /run/dev/sda1 20G 9.9G 9.2G 52% /tmpfs 511M 44M 468M 9% /dev/shmtmpfs 5.3M 0 5.3M 0% /run/locktmpfs 511M 0 511M 0% /sys/fs/cgrouptmpfs 103M 8.2k 103M 1% /run/user/1000

Příkazy grep a awk rozeberou výstup na tento:

0% udev52% /dev/sda1

Potřebujeme kontrolovat výstup z df, abychom mohli řídit naše testovací případy. Nechceme, aby se výsledek kontroly lišil podle skutečného využití disku v systému, kde testovací sadu spouštíme. Nekontrolujeme využití disku, kontrolujeme logiku skriptu. Při spuštění skriptu v produkčním prostředí se bude kontrolovat využití disku. To, co zde děláme, slouží k ověřování, nikoli k produkčnímu provozu. Proto potřebujeme falešný nebo „maketový“ příkaz df, pomocí kterého můžeme generovat „testovací data“ pro každý případ.

Na platformě *nix je možné přepsat skutečný příkaz df definováním aliasu. Chceme, aby aliasovaný příkaz vypisoval testovací hodnoty ve stejném formátu jako výstup příkazu df -HP. Zde je jeden ze způsobů, jak to udělat (je to celé na jednom řádku; níže je to rozděleno kvůli čitelnosti):

alias df="shift;echo -e 'Filesystem Size Used Avail Use% Mounted on'; echo -e 'tempfs 511M 31M 481M 6% /dev/shm'; echo -e '/dev/sda1 20G 9.9G 9.2G 52% /'"

Přesun při spuštění skriptu přeskočí argument ‚-HP‘, takže si systém nebude stěžovat, že -HP je neznámý příkaz. Aliasovaný příkaz df vypisuje výstup ve stejném tvaru jako df -HP.

Testovací hodnoty jsou při spuštění skriptu odeslány do grepu a následně do awk, takže se vysmíváme jen minimu potřebnému pro kontrolu našich testovacích případů. Chceme, aby náš testovací případ byl co nejblíže „skutečné věci“, aby nedocházelo k falešně pozitivním výsledkům.

Mocky vytvořené pomocí knihovny pro mocking mohou při volání vracet předem definovanou hodnotu. Náš přístup k mockování příkazu df odráží tuto funkci mockingu; zadáváme předem definovaný výstup, který má být vrácen, kdykoli testovaný kód zavolá df.

Mocking the mail Command

Chceme vědět, zda se skript pokusí odeslat e-mail za správných podmínek, ale nechceme, aby někam odeslal skutečný e-mail. Proto chceme příkaz mail aliasovat, stejně jako jsme to dříve udělali s příkazem df. Potřebujeme nastavit něco, co budeme moci kontrolovat po každém testovacím případu. Jednou z možností je zapsat při volání příkazu mail nějakou hodnotu do souboru a tu pak kontrolovat v našem testovacím případu. To je znázorněno v následujícím příkladu. Možné jsou i jiné metody.

Mocky vytvořené pomocí knihovny mocking mohou počítat, kolikrát je testovaný kód zavolá, a my můžeme tvrdit očekávaný počet volání. Náš přístup k mockování příkazu mail odráží tuto funkci mockingu; pokud je po spuštění skriptu diskusage.sh v souboru mailsent přítomen text „mail“, znamená to, že skript skutečně zavolal příkaz mail.

Vzor pro provádění automatických kontrol

Automatické nebo spustitelné kontroly na libovolné úrovni abstrakce, pro libovolný druh aplikace nebo skriptu v libovolném jazyce, obvykle zahrnují tři kroky. Ty se obvykle jmenují:

  • Uspořádání
  • Úkon
  • Potvrzení

Důvodem je pravděpodobně to, že všichni milují aliterace, zejména na písmeno A, protože samotné slovo „aliterace“ začíná písmenem A.

Ať už je důvod jakýkoli, v kroku uspořádání nastavujeme předběžné podmínky pro náš testovací případ. V kroku act vyvoláme testovaný kód. V kroku assert deklarujeme výsledek, který očekáváme.

Pokud používáme testovací framework nebo knihovnu, nástroj za nás krok assert pěkně zpracuje, takže nemusíme v našich testovacích sadách kódovat spoustu těžkopádné logiky if/else. Pro náš úvodní příklad zde nepoužíváme testovací framework ani knihovnu, takže kontrolujeme výsledky každého případu pomocí bloku if/else. V příštím díle si pohrajeme s unit testovacími frameworky pro shellové jazyky a podíváme se, jak to vypadá.

Tady je náš hrubý, ale účinný testovací skript pro testování Vivekova shellového skriptu, který jsme pojmenovali diskusage.sh:

#!/bin/bashshopt -s expand_aliases# Before allalias mail="echo 'mail' > mailsent;false"echo 'Test results for diskusage.sh' > test_resultstcnt=0# It does nothing when disk usage is below 90%# Before (arrange)alias df="echo 'Filesystem Size Used Avail Use% Mounted on';echo '/dev/sda2 100G 89.0G 11.0G 89% /'"echo 'no mail' > mailsent# Run code under test (act). ./diskusage.sh# Check result (assert)((tcnt=tcnt+1))if ]; then echo "$tcnt. FAIL: Expected no mail to be sent for disk usage under 90%" >> test_resultselse echo "$tcnt. PASS: No action taken for disk usage under 90%" >> test_resultsfi # It sends an email notification when disk usage is at 90%alias df="echo 'Filesystem Size Used Avail Use% Mounted on';echo '/dev/sda1 100G 90.0G 10.0G 90% /'"echo 'no mail' > mailsent. ./diskusage.sh((tcnt=tcnt+1))if ]; then echo "$tcnt. PASS: Notification was sent for disk usage of 90%" >> test_resultselse echo "$tcnt. FAIL: Disk usage was 90% but no notification was sent" >> test_resultsfi # After allunalias dfunalias mail# Display test results cat test_results

Tady je průchod testovacím skriptem.

Nejprve vidíte, že používáme bash k testování obyčejného starého souboru .sh. To je naprosto v pořádku. Není to nutné, ale je to v pořádku.

Dále vidíte příkaz shopt. Ten způsobí, že shell při vyvolání podskupiny pro spuštění skriptu diskusage.sh rozšíří naše testovací aliasy. Ve většině případů použití bychom aliasy do podprocesorů nepředávali, ale testování jednotek je výjimkou.

Komentář „Přede vším“ je určen lidem, kteří znají rámce pro testování jednotek, které mají příkazy pro nastavení a zrušení. Ty se často jmenují nějak jako „před“ a „po“ a obvykle existuje jeden pár, který dává do závorky celou sadu testů, a další pár, který se provádí jednotlivě pro každý testovací případ.

Chtěli jsme ukázat, že definování aliasu pro mail, inicializace souboru s výsledky testů a inicializace počítadla testovacích případů se provádí přesně jednou, na začátku sady testů. Takové věci jsou ve spustitelných testovacích sadách normální. Skutečnost, že testujeme shellový skript místo aplikačního programu, na tom nic nemění.“

Další komentář „Nic to nedělá…“ označuje začátek našeho prvního individuálního testovacího případu. Většina frameworků pro jednotkové testy nabízí možnost uvést název každého případu, abychom měli přehled o tom, co se děje, a aby ostatní nástroje mohly z různých důvodů vyhledávat, filtrovat a extrahovat testovací případy.

Následuje komentář, který zní: „Před (uspořádat)“. Ten představuje nastavení, které se vztahuje pouze na jeden testovací případ. Nastavujeme alias df tak, aby vypisoval výstup, který potřebujeme pro tento konkrétní případ. Do souboru také vypíšeme text „no mail“. Tak budeme schopni zjistit, zda se skript diskusage.sh pokusil odeslat oznamovací e-mail.

Následuje krok jednání, ve kterém testovaný kód procvičíme. V tomto případě to znamená spuštění samotného skriptu diskusage.sh. Namísto jeho přímého spuštění jej zdrojujeme .

Nyní provedeme krok assert, který v tomto příkladu provádíme složitým způsobem, protože jsme zatím nezavedli testovací framework. Zvýšíme počítadlo testů, abychom mohli očíslovat testovací případy v souboru s výsledky. V opačném případě, pokud bychom měli velké množství případů, by mohlo být obtížné zjistit, které z nich selhaly. Testovací rámce to řeší za nás.

Alias, který jsme definovali pro příkaz mail, zapisuje text ‚mail‘ do souboru mailsent. Pokud diskusage.sh zavolá mail, pak soubor mailsent bude obsahovat ‚mail‘ místo původní hodnoty ‚no mail‘. O tom, jaké jsou podmínky vyhovění a nevyhovění, se můžete přesvědčit přečtením řetězců, které se ozývají do souboru s výsledky testů.

Začneme komentářem „Odešle e-mailové oznámení…“ a zopakujeme kroky arrange, act, assert pro další testovací případ. Tentokrát necháme náš falešný příkaz df emitovat jiná data, aby řídil jiné chování testovaného kódu.

Kde se objeví komentář „After all“, uklidíme po sobě tím, že odstraníme definice, které jsme vytvořili v nastavení „Before all“ v horní části testovacího skriptu.

Nakonec vypíšeme obsah souboru test_results, abychom viděli, co jsme získali. Vypadá takto:

Test results for diskusage.sh1. PASS: No action taken for disk usage under 90%2. PASS: Notification was sent for disk usage of 90%

Proč používat testovací framework/knihovnu?“

Právě jsme napsali několik jednotkových testů pro shellový skript, aniž bychom použili testovací framework, knihovnu mocking nebo knihovnu assertion. Zjistili jsme, že systémové příkazy lze zesměšnit definováním aliasů (alespoň na systémech *nix), že aserce lze implementovat jako podmíněné příkazy a že základní strukturu jednotkového testu lze snadno nastavit ručně.

Nebylo těžké to udělat bez frameworku nebo knihovny. Jaký je tedy přínos?“

Testovací rámce a knihovny zjednodušují a standardizují testovací kód a umožňují mnohem čitelnější testovací sady než ručně vytvořené skripty obsahující spoustu podmíněných příkazů. Některé knihovny obsahují užitečné doplňkové funkce, například možnost zachycovat výjimky nebo možnost psát testovací případy řízené tabulkami a daty. Některé jsou přizpůsobeny pro podporu konkrétních produktů zajímavých pro infrastrukturní inženýry, například Chef a Puppet. A některé obsahují funkce pro sledování pokrytí kódu a/nebo formátování výsledků testů do podoby využitelné nástroji v CI/CD pipeline nebo alespoň webovým prohlížečem.

Rámce jednotkových testů pro skripty

V tomto seriálu se budeme zabývat několika rámci jednotkových testů pro shellové skripty a skriptovací jazyky. Zde je jejich přehled:

  • shunit2 je velmi solidní Open Source projekt s desetiletou historií. Původně jej vyvinula Kate Ward, inženýrka a manažerka spolehlivosti webu ve společnosti Google se sídlem v Curychu, a aktivně jej vyvíjí a podporuje tým šesti lidí. Od skromných začátků jako bodového řešení pro testování knihovny pro logování shellových skriptů se cíleně vyvinul v univerzální framework pro jednotkové testy, který podporuje více shellových jazyků a operačních systémů. Obsahuje řadu užitečných funkcí nad rámec jednoduchých tvrzení, včetně podpory datově řízených a tabulkově řízených testů. Používá tradiční styl tvrzení „assertThat“. Stránky projektu obsahují vynikající dokumentaci. Pro univerzální jednotkové testování shellových skriptů je to mé hlavní doporučení.
  • BATS (Bash Automated Testing System) je framework pro jednotkové testy jazyka bash. Vytvořil ho Sam Stephenson asi před sedmi lety a podílel se na něm asi tucet přispěvatelů. Poslední aktualizace proběhla před čtyřmi lety, ale to není důvod k obavám, protože tento druh nástroje nevyžaduje časté aktualizace ani údržbu. BATS je založen na protokolu TAP (Test Anything Protocol), který definuje konzistentní textové rozhraní mezi moduly v jakémkoli druhu testovacího svazku. Umožňuje čistou a konzistentní syntaxi v testovacích případech, i když se nezdá, že by přidával mnoho syntaktického cukru nad rámec přímých příkazů bashe. Neexistuje například žádná speciální syntaxe pro tvrzení; pro výsledky testů píšete příkazy bash. S ohledem na to může jeho hlavní hodnota spočívat v logickém uspořádání sad testů a případů. Všimněte si také, že psaní testovacích skriptů v bashi nám nebrání testovat skripty, které nejsou v bashi; to jsme udělali dříve v tomto příspěvku. Skutečnost, že syntaxe BATS je tak blízká syntaxi jazyka bash, nám dává velkou flexibilitu při práci s různými jazyky shellu v našich testovacích sadách, a to i za cenu možné ztráty čitelnosti (záleží na tom, co považujete za „čitelné“; cílové publikum tohoto příspěvku pravděpodobně považuje syntaxi prostého jazyka shellu za docela čitelnou). Jednou z obzvláště zajímavých funkcí (podle mého názoru) je možnost nastavit textový editor se zvýrazněním syntaxe pro BATS, jak je zdokumentováno na wiki projektu. K datu vydání tohoto příspěvku byly podporovány Emacs, Sublime Text 2, TextMate, Vim a Atom.
  • zunit (ne ten od IBM, ale ten druhý) je framework pro unit testy zsh, který vyvinul James Dinsdale. Na stránkách projektu se uvádí, že zunit byl inspirován BATS, a obsahuje velmi užitečné proměnné $state, $output a $lines. Má však také definitivní syntaxi tvrzení, která se řídí vzorem „assert actual matches expected“. Každý z těchto frameworků má některé jedinečné vlastnosti. Zajímavou vlastností ZUnit je podle mého názoru to, že označí všechny testovací případy, které neobsahují tvrzení, jako „rizikové“. Můžete to přepsat a vynutit spuštění případů, ale ve výchozím nastavení vám framework pomáhá pamatovat na to, abyste do každého testovacího případu zahrnuli tvrzení.
  • bash-spec je behaviorální testovací framework, který podporuje pouze bash (nebo byl alespoň testován pouze proti skriptům v bashi). Je to můj skromný vedlejší projekt, který existuje přes čtyři roky a má několik „skutečných“ uživatelů. Není příliš aktualizován, protože v současné době dělá to, k čemu byl určen. Jedním z cílů projektu bylo využívat funkce bashe ve „fluidním“ stylu. Funkce se volají postupně, přičemž každá předá celý seznam argumentů další poté, co spotřebuje tolik argumentů, kolik jich potřebuje ke splnění svého úkolu. Výsledkem je čitelná sada testů s příkazy jako „expect package-name to_be_installed“ a „expect arrayname not to_contain value“. Pokud se používá jako vodítko pro vývoj skriptů zaměřených na testování, má jeho návrh tendenci vést vývojáře k psaní funkcí, které podporují myšlenku „modularity“ nebo „jediné odpovědnosti“ nebo „oddělení obav“ (říkejte tomu, jak chcete), což vede ke snadné údržbě a snadno použitelným funkcím. „Behaviorální styl“ znamená, že tvrzení mají podobu „očekávej, že toto bude odpovídat tamtomu“.
  • korn-spec je port bash-spec pro shell korn.
  • Pester je framework pro unit testy pro Powershell. Powershell vypadá a působí spíše jako aplikační programovací jazyk než čistě skriptovací jazyk a Pester nabízí plně konzistentní vývojářské prostředí. Pester je dodáván se systémem Windows 10 a lze jej nainstalovat do jakéhokoli jiného systému, který podporuje Powershell. Má robustní knihovnu assertion, vestavěnou podporu pro mocking a shromažďuje metriky pokrytí kódu.
  • ChefSpec staví na rspec a poskytuje behaviorální testovací rámec pro recepty Chef. Chef je aplikace v jazyce Ruby a ChefSpec plně využívá možností rspec a navíc má zabudovanou podporu pro funkce specifické pro Chef.
  • rspec-puppet je framework behaviorálního stylu pro Puppet, funkčně podobný ChefSpec.

admin

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.

lg