Hlavní navigace

Automatické testování webových aplikací: funkcionální testování podrobně

Pavel Herout

Třetí z minisérie článků popisujících zkušenosti získané při testování webové aplikace pomocí automatických funkcionálních testů. Seznámíte se s podrobnostmi testování, dobrými radami a celkovým shrnutím.

Doba čtení: 11 minut

V předchozím článku byla stručně charakterizována testovaná aplikace a věřím, že si ji někteří čtenáři i vyzkoušeli.

Jak již bylo zmíněno v prvním dílu, používal jsem pro testování Selenium WebDriver (jehož verze se průběžně měnily, poslední použitá byla 3.14). Testy jsem psal v Javě v Eclipse a využíval jsem hojně možností frameworku JUnit 4.

Hned na začátku je nutné zmínit problematiku organizace testů. Dopředu bylo jasné, že testů bude mnoho a mým cílem bylo umožnit jednoduše spouštění všech testů najednou, jejich skupin, podskupin skupin atd. až do úrovně jednotlivých testovacích tříd. Případně při vývoji testů i jednotlivých testovacích případů, což IDE Eclipse umožňuje. Kromě toho se ukázalo jako výhodné vytvářet i skupiny testů napříč hierarchií, např. všechny testy modálních oken. Pro účely organizace testů dává JUnit4 velmi dobré možnosti díky anotaci  @Suite.

@RunWith(Suite.class)
@Suite.SuiteClasses({
  All_Passive_Tests.class,
  All_Active_Tests.class,
  All_Negative_Tests.class,
})

Celou sadu funkcionálních testů lze rozdělit na dvě části. V první části (balík support) jsou všechny pomocné třídy, které budou testy využívat. Většinou mají formu knihovních tříd, tj. pouze statické metody nebo konstanty. Po mnoha refaktorizacích je základní struktura balíku support následující:

basic  – nejzákladnější soubory se seznamy ID a pro zpracování konfigurace

db  – sestava tří podbalíků s celkově 14 třídami. Tento balík víceméně supluje funkčnost databázové vrstvy testované webové aplikace. Tento přístup by se dal označit jako varianta N-version programming.

test  – pomocné třídy a rozhraní, zejména pro kategorizaci testů. To pak umožní díky anotaci @IncludeCategory spouštět jen vybrané podskupiny (kategorie) testů

@RunWith(Categories.class)
@IncludeCategory(ModalContentTest_Category.class)
@SuiteClasses(All_Tests.class)

utils  – nástroje pro práci s webem rozšiřující možnosti Selenia WebDriver. Má dva podbalíky:

  • web  – třídy pro práci se základními elementy webové aplikace, např. nalezení tlačítka podle ID, kliknutí na něj a čekání na odezvu
  • application  – vysokoúrovňové aktivity přímo na míru testované aplikace, např. „v tabulce zapsaných předmětů zruš předmět daného jména“

Díky kvalitní funkcionalitě tříd z balíku utils lze pak psát velmi jednoduše a přehledně vlastní testy.

Poznámka: Některé třídy z balíků db a utils jsou otestovány jednotkovými testy, což se ukázalo jako výhodné. Tyto testy nebudou dále zmiňovány a nejsou započítávány do statistik žádných testů.

Druhá část sady funkcionálních testů jsou již vlastní testy. Ty lze rozdělit do tří základních skupin, které jsou uloženy ve stejnojmenných balících:

1. passive  – testy zjišťující, zda je vše nastaveno, tak jak je očekáváno. Žádný z těchto testů nemění stav testované aplikace. Pomocí kategorií testů se dají dále dělit na (seřazeno podle časové náročnosti testů):

  • statický obsah webové stránky – zjišťují, zda jsou na testované stránce všechny požadované elementy, mají správné ID, správný popis, správnou viditelnost a tooltip. Totéž je zjišťováno pro položky menu.
  • statický obsah modálního okna – totéž, ale pro elementy v modálním okně
  • obsah DB – zjišťují, zda přednastavení testované webové aplikace odpovídá specifikaci. Konkrétně zjišťují jména všech uživatelů, kombinace vyučovaných či zapsaných předmětů a zkoušek.
  • obsah DB v modálních oknech – podobně, ale pro obsahy tabulek v modálních oknech. Například na každý předmět je zapsáno několik studentů a jejich jmenný seznam se zobrazí v tabulce v modálním okně. Tyto testy jsou časově nejnáročnější.

Po průchodu všech pasivních testů si můžeme být jisti, že počáteční stav aplikace je bezchybný. Pokud by bylo možné použít termín „pokrytí testy“, pak zde je 100%.

2. active  – testy, kdy je měněn stav testované aplikace. Dají se dělit do tří skupin:

  • jedna jediná aktivita – je provedena pouze jedna aktivita a následně jsou testovány všechny její očekávané dopady. Například pokud si student zruší termín zkoušky, musí se tato informace objevit na několika místech u dotyčného studenta, ale i u učitele, který daný předmět vyučuje.

Byly prověřeny všechny jednotlivé aktivity a k nim všechny jejich známé konsekvence.

  • testy hraničních podmínek – jako předchozí testy, ale v okrajových případech. Například pokud si student odepíše svůj jediný zapsaný předmět, musí se mu zobrazit zcela jiná (tj. prázdná) tabulka předmětů.

Byly prověřeny všechny jednotlivé aktivity a k nim všechny jejich známé konsekvence.

  • testy podle scénářů – provádí se několik akcí po sobě i v kombinacích student – učitel. Například student si zapíše předmět, učitel na tento předmět vypíše zkoušku, student se přihlásí na tento termín zkoušky atd.

Byly prověřeny nejběžnější scénáře. Všechny možné kombinace nelze samozřejmě provést staticky napsanými testy. Zde je příležitost pro dynamicky generované testy.

3. negative  – negativní testy (testy selháním). Protože UIS poskytuje jen velmi omezené možnosti uživatelských vstupů, kdy lze zadat libovolný údaj, je těchto testů jen omezený počet. Testována byla opět všechna známá potenciální rizika.

Rozsahy testů i podporujícího kódu jsou zřejmé z následující tabulky. Významy jednotlivých sloupců jsou stejné, jako u dříve uvedené tabulky. Poslední sloupeček udává počet testů, tj. porovnání dvou veličin (assertů).

  celkově řádek řádek kódu testů
support 4 991 3 233
passive 5 214 3 282 456
active 4 800 3 146 547
negative 1 111 780 63
dohromady 16 116 10 441 1 066

Lessons learned aneb co jsme se naučili při detailním testování

Testování je náročné

Otestovat zodpovědně i jednoduchou aplikaci je opravdu náročné. Když ale programátoři vytvoří lépe testovatelný kód, je to o něco snazší (viz dále).

Zásadní výhoda automatizovaných testů

Tou je snadná a rychlá opakovatelnost testů a její význam je dán realitou webových prohlížečů. Desktopovou aplikaci prakticky stačí otestovat pouze jednou a – pokud použiji mírnou nadsázku – dokud se nezmění operační systém, bude desktopová aplikace fungovat pořád stejně. Operační systémy se nemění příliš často.

Webových prohlížečů je ale mnoho typů, minimálně pět až šest těch nejdůležitějších. Už jen možnost otestovat automaticky aplikaci podle jednoho kódu testů na všech prohlížečích je životně důležitá. Navíc – a to je nutné zdůraznit – verze prohlížečů se mění velmi rychle a uživatel má často nastaven automatický upgrade. Čili webová aplikace, která uživateli včera večer v jeho oblíbeném prohlížeči fungovala, už druhý den ráno fungovat nemusí. Rychlost postřehnutí tohoto problému je pro provozovatele aplikace důležitá.

Protože Selenium WebDriver využívá skutečný prohlížeč, lze velmi snadno (za cenu strojového času) ověřit, že konkrétně v něm funguje vše, jak má. A to opakovat pro další typ prohlížeče. A relativně snadno by bylo možné připravit nadstavbu nad testy, které by pak byly opakovaně spouštěny pro různé verze prohlížečů. S příchodem nové verze prohlížeče to pak pouze znamená získat pro něj novou verzi driveru (což někdy není nutné, protože drivery fungují pro více verzí) a spustit sadu automatizovaných testů. Čili zjištění, že aplikace bude korektně fungovat v právě nové verzi prohlížeče, je otázka chvíle příprav a pak doby běhu automatických testů.

Další výhodou automatizovaných testů jsou situace, kdy (téměř) nelze použít manuální testování. To je typicky u výkonnostních či zátěžových testů. (Zde upřímně přiznávám, že jsem žádné výkonnostní testování neprováděl, takže pouze teoretizuji.) Výkonnostní testy by měly být provedeny nejen při prvním release, ale pak i při každém dalším. Manuálně se toto provádí a měří těžko a je to drahé, automatizací můžeme testy kdykoliv zadarmo pustit znovu a s mnohem větší přesností dosažených výsledů.

Testování je sice náročné, ale vývojář jej může usnadnit

Tester, který píše automatizované testy, odvádí principiálně stejně kvalifikovanou práci jako vývojář. Ten mu (jako kolegovi) může výrazně pomoci tím, že napíše svůj kód testovatelný. Naprosté minimum je dodat každému elementu jeho unikátní ID (případně – tam kde není ID možno použít, např. u Vaadin – pak jiný způsob pro jednoznačnou identifikaci). ID by měla být v celé aplikaci tvořena podle jednotného schématu. Pokud jsou v testech adresovány elementy pomocí jejich ID, výrazně to sníží „křehkost“ těchto testů, tj. při dílčích změnách layoutu není třeba testy měnit buď vůbec, nebo jen velmi málo.

Dále vývojář rozhodně testera (a nejen jeho) potěší tím, že stejné věci budou nejen navenek vypadat stejně, ale i vnitřně (tj. HTML kód) budou stejné. Například všechny tabulky budou mít záhlaví pomocí <th>, což se může mnoha lidem jevit jako samozřejmé, nebo všechna modální okna bude možné uzavřít pomocí jednotně identifikovaného elementu.

Co se mně vyplatilo použít

Mít konfigurační soubor, kde budou minimálně tři věci.

  1. Bázové URL testované aplikace
  2. Volba testovacího webového prohlížeče minimálně ze dvou webových prohlížečů
  3. Úplné cesty na (seleniové) drivery příslušných webových prohlížečů.
#URL_Base=http://localhost:8080/uis
URL_Base=http://oks.kiv.zcu.cz:10008

WebBrowserType=Chrome
#WebBrowserType=Opera
#WebBrowserType=Mozilla

ChromeDriverURI=c:/Program Files/Java/selenium/chromedriver2-44.exe
OperaDriverURI=c:/Program Files/Java/selenium/operadriver2-40.exe
MozillaDriverURI=c:/Program Files/Java/selenium/geckodriver0-23-0.exe

Webové prohlížeče totiž mají většinou automatizované aktualizace, ale jejich seleniové drivery samozřejmě nikoliv. Takže pokud vám najednou přestane všechno fungovat, máte možnost jednoduchou změnou spustit funkcionální testy pomocí jiného prohlížeče. A je velmi nepravděpodobné, že by se oba prohlížeče současně aktualizovaly tak, že „vypadnou“ z rozsahu podporovaného příslušným driverem. Konkrétně já jsem používal prohlížeče Chrome (primárně), Firefox a Opera. Od března 2018 do listopadu 2018 se driver pro Chrome změnil osmkrát (verze 2–37 až 2–44).

Připravte si (abstraktní) třídu, od které budou všechny testy dědit. Úkolem této třídy je načtení konfigurace (viz předchozí bod), tj. inicializace příslušného webového prohlížeče v Seleniu. Toto je časově náročná operace a měla by tedy proběhnout pouze jednou, ať již spouštíte jeden test nebo skupinu testů. A na konci by měla příslušný ovladač (potažmo webový prohlížeč) standardně ukončit.

Aby bylo možné spouštět testy jednotlivě i po skupinách, využijte z JUnit4 anotace @Category a @Suite.SuiteClasses. Možností volit granulitu spouštěných testů výrazně zrychlíte jejich ladění.

Jakmile máte zpracovanou konfiguraci a granulitu testů a celé je to ověřené na jednoduchých testech, nepouštějte se do psaní dalších testů. Místo toho se snažte napsat co nejuniverzálnější podpůrný kód k budoucím testům (balík support). Použitím kvalitního support výrazně zrychlíte následné psaní skutečných testů a zpřehledníte je. A pokud ještě kód support otestujete jednotkovými testy, bude to skvělé, protože při refaktoringu, ke kterému určitě dojde, využijete výhod těchto regresních testů.

Parametrizované testy (anotace JUnit4 @Parameters) jsou významným pomocníkem pro zkrácení a zpřehlednění kódu.

Vlastní provádění testů můžete značně zrychlit tím, že použijete možnost definovat pořadí testů ( @FixMethodOrder v JUnit4. Zde pozor na JUnit 5.0, která tuto možnost nemá – přichází plnohodnotně až v JUnit 5.4.). Psát testy, které jsou závislé na sobě navzájem (tj. které musejí běžet v určitém pořadí), není obecně dobrý nápad. Ale když se uváží situace, kdy se provede u studenta jedna aktivita a vzápětí se kontroluje její dopad na všech možných místech, pak je rozhodně rychlejší, zkontrolovat vše potřebné najednou u již přihlášeného studenta a pak najednou u následně jednou přihlášeného učitele. Střídavé přihlašování a odhlašování studenta a učitele při náhodném pořadí testů je zbytečné a podstatně zdržuje.

Chrome je možné spustit v headless módu. Podle mých pokusů to ale čas průběhu testů příliš nezkrátí (1836 sec oproti 1800 sec v headless módu).

Protože elementů typu input, kdy uživatel může zadat libovolný údaj, je v aplikaci minimum, je také zapotřebí omezené množství negativních funkcionálních testů. Celkově jich bylo pouze 6 %.

Závěrečné poznámky

  • Co se týče časové náročnosti přípravy testů, odhaduji, že je zhruba stejně velká, jako vývoj aplikace. Takže kdybychom celou akci opakovali, tak se současnými zkušenostmi bych sadu testů vyvinul zhruba za stejnou dobu jako by trval vývoj aplikace.
  • V této souvislosti je třeba poznamenat, že s vytvářením testů, respektive s vytvářením podpůrných knihoven (balík support) je třeba začít již od samého začátku. Zlepší to i strukturu vyvíjené aplikace.
  • Nemyslím si, že by testy GUI byly tak „křehké“, jak se obecně udává. Pokud jsou důsledně používány ID (viz výše) a je dobře připraven balík support (viz též výše), jsou GUI testy až překvapivě stabilní. To jsem prakticky vyzkoušel na přidání jedné funkcionality do učitelova menu.
    Samozřejmě ale mluvím o situaci, kdy už je proveden poměrně detailní návrh aplikace, který se mění jen drobně. Pokud by došlo k radikální změně, např. používané tabulky by se nahradily něčím jiným, pak by bylo ovšem nutné většinu testů napsat znovu.
  • Aktivních testů je 547, což je značné číslo. Ovšem pokud se zamyslím nad jejich složením, nejsem toho názoru, že by se v reálné praxi jich mohlo větší množství vynechat. V naprosté většině to jsou v podstatě testy základní funkcionality, kdy se provede jedna akce a zkoumají se její dopady. Pro jistotu zde ještě jednou zdůrazním, že při provedení jedné jediné aktivity se následně kontroluje její dopad na např. deseti místech. Takže ve mnou použité terminologii jsem napsal deset testů (asertů). Ovšem z jiného pohledu jsem vlastně otestoval jen jeden jediný ovládací prvek. Z tohoto důvodu vyplývá mé přesvědčení, že nelze počet testů příliš omezit, protože ovládací (aktivní) prvky prostě musíme vyzkoušet všechny. Můžeme však, chtěli-li bychom šetřit, omezit počet testů všech jejich dopadů.
  • Automatizované testy prakticky znamenají vyvíjet paralelně minimálně stejně velkou (ale ne nutně stejně složitou) aplikaci (viz výše podbalík db). Tuto druhou aplikaci je sice možné testovat (minimálně jednotkovými testy), ale pravděpodobně to málokdo udělá. Jediným důkazem korektnosti je tedy využití již zmíněného principu N-version programming, kdy soulad testovací a testované aplikace poskytuje přiměřenou míru jistoty správnosti obou.
  • Nahrávané skripty pomocí Selenium IDE (či podobných nástrojů) mohou být v dílčích případech jednotlivě užitečné, ale nedokážu si představit vytvoření jejich varianty pro popisované testy.
  • Nedokážu udělat zobecnění na nějakou opravdu velkou webovou aplikaci.

Na úplný závěr bych rád dodal, že uvítám praktické rady zkušenějších, které bych pak promítl do výuky. Na druhou stranu jsem ochoten poradit těm méně zkušeným.

Našli jste v článku chybu?