Hlavní navigace

Jak jsem (znovu) testoval webovou aplikaci

Pavel Herout

Článek popisuje „extrémní“ případ testování v experimentálním projektu, kdy jsem se snažil napsat tolik (smysluplných) automatizovaných testů, kolik jen bylo možné. To pro běžné projekty je samozřejmě zbytečný luxus.

Doba čtení: 15 minut

Sdílet

Tato minisérie článků bude volně navazovat na předchozí minisérii „Co to znamená ‚pořádně‘ otestovat webovou aplikaci“, která vyšla na Root.cz v lednu a únoru 2019.

Důležité omezení

Vše, co je v této minisérii popisováno, představuje automatizované funkcionální testování černé skříňky.
Z tohoto pohledu je tedy třeba jasně zdůraznit, že z dimenze kvality FURPS+ (Functionality, Usability, Reliability, Performance, Supportability) jsem se zabýval jen tou první částí, tj. funkcionalitou. Samozřejmě ale to, že jsem neprováděl testy podle ostatních dimenzí kvality, neznamená, že takovéto testy považuji zbytečné.

Článek popisuje „extrémní“ případ testování v experimentálním projektu, kdy jsem se snažil napsat tolik (smysluplných) atomatizovaných testů, kolik jen bylo možné. To pro běžné projekty je samozřejmě zbytečný luxus.

Úvod

Na samém začátku je třeba vysvětlit, proč jsem testoval znovu, když předchozí testy byly poměrně důkladné. Důvodem napsání víceméně zcela nové sady testů byl přechod na testovací framework JUnit5 (z původně použitého JUnit4) a hlavně důsledné použití návrhového vzoru PageObject z frameworku Selenia. Změny byly tak rozsáhlé, že nemělo význam pouze refaktorovat předchozí sadu testů, ale bylo výhodnější napsat vše téměř od samého začátku. Samozřejmě jsem k tomu využil všech předtím získaných (zejména negativních) zkušeností – viz dále. Výsledkem by měla být rozsáhlá sada testů, za které se už „nemusím stydět“ ;-) a zájemce si je může prohlédnout na: https://gitlab.kiv.zcu.cz/herout/TbUIS-PageObject

Testovaná aplikace zůstala stejná. Její popis si v případě zájmu můžete přečíst v Automatické testování webových aplikací: souhrnný přehled výsledků
Aplikaci si můžete i sami vyzkoušet.

V tomto článku popíši celkové výsledky, pak strukturu podpůrné knihovny a strukturu testů a významnější informace o jejich vzniku a struktuře. Následující článek bude věnován problematice návrhového vzoru PageObject. A poslední článek této minisérie bude popisovat některé vymoženosti frameworku JUnit5.

Cílem všech tří článků je poskytnutí podrobnější informace podpořené reálnými údaji o možnostech testování rozsahem menší až střední webové aplikace, nárocích na toto testování a dalších postřehů z vývoje testů. Tyto informace mohou být užitečné pro manažery projektů, např. pro rozhodnutí, zda se do automatizovaného testování pustit tímto způsobem, případně pro představu o možné náročnosti. Pokud jste testeři, pak pro vás mohou být zajímavé informace o nových možnostech JUnit5, o konceptu PageObject nebo o možné struktuře projektu.

No, a pokud nic z toho nevyužijete a webové aplikace již spokojeně dlouhodobě automatizovaně testujete, pak si můžete po přečtení říci, že to děláte lépe než já  ;-)

Co jsem v této verzi oproti původní změnil z hlediska psaní kódu

Stručně řečeno – dodržoval jsem zásady tvorby kódu, které sice své studenty učím, ale které jsem z lenosti nedělal buď vůbec, nebo ne dost důsledně  ;-)

1. Logování

Nejvýraznější změnou je důsledné logování uplatňované na všech úrovních a hned od samého počátku, tj.od čtení konfiguračního souboru. Toto bych viděl jako nejvýraznější změnu konceptu, která více či méně ovlivnila všechny další části kódu. Změnou konceptu míním stav, kdy jsem se snažil o jednotné logování, kterému jsem přizpůsobil i strukturu kódu (podrobně viz např. v PageObject).

Pro logování je využit framework Log4J v. 2.11.1 řízený konfiguračním souborem v XML ( logger-config.xml). Jsou samozřejmě využity různé úrovně logování. Logovací informace se zapisují důsledně pomocí jen několika málo obalujících metod (např. Fail.failTestDueToMissingElement()), což zajišťuje unifikovanou logovanou informaci. Ta se zapisuje do souboru  test-results-log.txt.

Speciální pozornost je věnována logování při načítání konfiguračních souborů, protože i drobné změny v konfiguraci činily v minulosti značné problémy. Výsledek je zapsán do souboru  test-config-log.txt.

2. Konfigurace seleniových web driverů z vnějšku

Tento přístup, kdy jsou úplné cesty k souborům jednotlivých driverů uvedeny v konfiguračním souboru, už byl uplatněn v předchozí verzi. V této verzi byl vylepšen.

3. Automatické snímání obrazovek

Tuto užitečnou činnost umožňuje Selenium ve spolupráci s knihovnou commons-io-2.6.jar. Obrazovky (v tomto případě chybové obrazovky) jsou snímány automaticky při vyhodnocení úspěšnosti testu (tj. při jeho selhání). Vzniklé snímky jsou ukládány do souborů pojmenovaných názvem testu a do adresáře s datem a časem spuštění testu. Celá tato informace je samozřejmě též logována. Protože (zvláště při ladění testů) může tato vlastnost obtěžovat, je možné ji snadno vypnout nastavením v konfiguračním souboru.

Poznámka:
Lze si představit i užitečné snímání nechybových obrazovek (tj. v případě, že test projde). Pasivní testy (viz dále) totiž spolehlivě odhalí, pokud na webové stránce něco chybí. Ale samozřejmě nedokáží odhalit, pokud je tam něco navíc, případně je něco jinak zobrazeno / naformátováno. Při potřebě testovat i tuto možnost si lze velmi snadno představit testy, které budou používat jednoduchou utilitu pro porovnání shodnosti dvou obrázků. Jedním z nich by byla vzorová obrazovka, druhým pak aktuálně sejmutá obrazovka.

Pro jistotu dodávám, že tento typ testů není v současné sadě testů použit, ale nebyl by problém jej v případě potřeby dodělat.

4. Javadoc komentáře

„Nemám čas psát komentáře!“ :-( Tento zavrženíhodný přístup jsem bohužel používal při vytváření předchozí verze testů a následné dopisování komentářů (aby se v kódu vyznali i spolupracující studenti) bylo skutečně otravné. Proto jsem se donutil psát Javadoc komentáře ihned. Jejich (značný) rozsah je patrný z tabulek rozsahů kódů (viz dále).

5. Jednotkové testy pro část support

Celý projekt testů je složen ze dvou základních částí – functional_tests, kde jsou vlastní testy, a support, kde jsou všechny podpůrné části. Rozsahy obou viz dále. Všude, kde to bylo smysluplné, jsem části kódu ověřoval jednotkovými testy. Ty jsou uloženy mimo základní strukturu projektu v balíku test (Maven struktura projektu). Tyto testy se nepočítají do žádných dále uváděných statistik funkcionálních testů.

Jednotkové testy se ukázaly jako neocenitelné při změnách v balíku support (jako regresní testy). Pokrytí (line coverage) příslušných částí kódu z balíku support je 88 % u support.test_oracle  a 75 % u  support.services.

6. Porovnávání jen omezeným počtem metod

V každém testu musí být porovnání skutečné a očekávané hodnoty (assert). V této sadě se porovnávání provádí pomocí několika málo metod ze třídy Check (např. Check.checkActualURL()). Tento přístup zajišťuje, že jsou v případě selhání testu zalogovány všechny požadované informace (zejména ID webového elementu) a to v jednotné struktuře. Navíc umožňuje jednoduše počítat proběhlé aserty – viz následující bod.

7. Jeden test může mít více assertů

Pokud píšeme jednotkové testy, pak je přístup více assertů v jednom testu většinou chybný, protože u jednotkových testů má jeden test pokrývat ideálně jen a pouze jednu právě testovanou vlastnost, tj. má jen jeden assert (jedno porovnání). U funkcionálních testů (po které je zde též využíván framework JUnit5) není mnohdy výhodné mít separátní test pro každý assert. U funkcionálních testů často potřebujeme najednou ověřit celý stav zkoumaného objektů. To v JUnit5 umožňuje metoda assertAll() (viz podrobně v části o JUnit5), která zajišťuje spuštění všech v ní obsažených assertů nezávisle na tom, zda některý (předchozí) z nich selže či nikoliv.

Díky tomu, že asserty lze snadno spočítat (a počet vlastních proběhlých testů eviduje samotný framework JUnit5), dostáváme po skončení běhu testů mnohem přesnější informaci o rozsahu testování. V současné testovací sadě běží 983 testů, v jejichž těle je provedeno 5 105 assertů.

Hlavní části struktury projektu

Zde bude popsán význam jednotlivých hlavních částí projektu. Balík pageobject pak bude podrobněji popsán ve druhé části této minisérie.

Jak již bylo zmíněno výše, tam, kde to bylo smysluplné, jsou kódy balíku support pokryty jednotkovými testy. Nemá smysl pokrývat těmito testy všechno, protože např. balík pageobject bude součástí funkcionálních testů, kde bude jeho funkcionalita dostatečně ověřena.

Dvě hlavní části jsou:

  • functional_tests – vlastní funkcionální testy – podrobně viz níže
  • support – všechny zbývající podpůrné kódy

Při pozorném pohledu zjistíte, že v této úrovni je ještě balík run_tests. V něm jsou uloženy dvě třídy pro externí spouštění funkcionálních testů, což vyžaduje podrobnější vysvětlení.

Pro vývoj celého projektu jsem používal IntelliJ IDEA a byl jsem s ním nadmíru spokojen. Protože pro „background“ funkcionálních testů využívám JUnit5, IntelliJ umí „inteligentně“ spouštět (z vývojového prostředí) jednotlivé testy, jednotlivé třídy testů, jednotlivé balíky testů atd. Ovšem pro spuštění testů je nutné vývojové prostředí. V okamžiku, kdy bude sada testů dokončena a odladěna, bude možné ji prohlásit za hotovou a pak bude výhodné ji moci spouštět buď z příkazové řádky, nebo z nějaké GUI nadstavby. A pro toto spouštění budou sloužit třídy z balíku  run_tests.

Tento způsob spuštění testů bude mít navíc tu výhodu, že bude možné ovlivnit běh spuštěných testů pomocí jejich štítků (tags). To IntelliJ, ve verzi kterou používám (2019.2.1 Community Edition), neumí (nebo jsem ještě nezjistil, jak by to měla uměla).

Je ověřeno, že spuštění testů pomocí tříd z balíku run_tests dává identické výsledky jako spuštění testů z prostředí IntelliJ. A to samozřejmě včetně logování a statistik testů, neboť tyto činnosti se provádějí jednotně (viz též TestWatcher v části o JUnit5).

Podrobně bude možnost externího spouštění testů popsána ve třetí části této minisérie o možnostech JUnit5.

Balík support


Tento balík je vlastně „knihovnou“ pro testy. Čím lépe je tato knihovna připravena, tím jednodušší a elegantnější budou vlastní testy.

Navíc nebude nutné omezovat se pouze na již existující funkcionální testy. Knihovna support se stane základem i pro další typy v budoucnu vyvíjených testů. Budou to testy podle scénářů, zátěžové testy, automaticky generované testy apod.

Hlavní části balíku:

  • basic – slouží pro načítání konfigurace a dále obsahuje třídy:
    • Const – základní konstanty celého projektu
    • Url – seznam všech URL v projektu
  • pageobjects – popis jednotlivých webových stránek pomocí příkazů Selenia. Díky tomuto přístupu se důsledně oddělí práce s webovými elementy stránky (seleniové příkazy), od kódu testů. Podrobně viz ve druhé části této minisérie.
  • selenium_utils – je to „nejnižší vrstva“ seleniových příkazů. Seleniové příkazy se běžně využívají v nadřazené vrstvě pageobjects. Ovšem pokud se některý příkaz v této vrstvě opakuje (typicky nastavení příslušné hodnoty výběrového seznamu, práce s check-boxem apod.), pak je vhodné tento kód umístit do nejnižší vrstvy a z vrstvy pageobjects jej pouze jednoduše volat. Podobně je tomu u složitějšího HTML kódu, kdy v aplikaci UIS je použito množství tabulek. Takže služby této vrstvy jsou typu: „klikni na tlačítko Enroll, které je ve stejné řádce jako předmět Programming in Java“.

Obsahuje třídy:

  • Click – všechna klikání v aplikaci včetně čekání na příslušnou odezvu

Poznámka:
Vyřešit správně veškerá čekání je komplikovaná záležitost, se kterou mi významně pomohly informace z knihy Mastering Selenium WebDriver, která bude zmíněna v části PageObject.

  • JavaSript – práce s JS v modálních oknech
  • Table – práce s jednoduchou tabulkou
  • SubTable – práce s vnořenými tabulkami
  • Utils – výběrové seznamy, check-boxy apod.
  • test_oracle – balík, ve kterém je uložena paralelní byznys logika projektu zajišťující „orákulum“ pro testy (nemá nic společného s databází Oracle)

Toto je diskutabilní záležitost, protože paralelní logika (N-version programming) je velmi pracná a může obsahovat skryté defekty. Testy by bylo možné vytvořit i bez ní, ovšem byly by mnohem méně elegantní, rozšiřitelné a přenositelné. Kód z balíku test_oracle totiž poskytuje v assertech automaticky (programově) očekávanou hodnotu, kterou by v případě neexistence balíku test_oracle bylo nutné stanovit „ručně“ / „natvrdo“.
Jednoduše řečeno – existence balíku test_oracle posouvá celé testování na „vyšší level“, což bude doufám patrné z ukázek v posledním dílu této minisérie.

Jak již bylo uvedeno výše, balík je důsledně otestován jednotkovými testy s mírou pokrytí řádek 88 %.

Pokud by vám připadalo programování paralelní logiky jako příliš náročné, pak dobrá zpráva je, že není nutné hned z počátku programovat byznys logiku 1:1 (s plnou funkčností). Bylo by možné vytvořit její určitou podmnožinu a tu v případě potřeby rozšiřovat.

  • services – nadstavba nad pageobject s využitím test_oracle

Pokud spojíme možnosti z balíků test_oracle a pageobject, můžeme nad jednotlivými webovými stránkami aplikace UIS vytvořit komplexní služby. Ty jsou dvojího typu. První jsou Actions, kdy „student Cyan si zapisuje předmět Programming in Java“. Druhé jsou Check, kdy „předchozí akce proběhla úspěšně – na stránce se zobrazí SuccessAlert“ a „předmět Programming in Java již není na této stránce nabízen k zápisu“.

Třídy a metody z tohoto balíku pak umožňují velmi elegantní a kompaktní zápis vlastních funkcionálních testů.

  • test_templates – šablony testů

Toto je vylepšení z JUnit5, které bude podrobně popsáno ve třetí části této minisérie. Ve stručnosti se jedná o to, že některé části webových stránek se stejně nebo téměř stejně opakují (hlavička, menu). Teoreticky by tyto části stačilo otestovat jen na jedné stránce a na ostatních stránkách předpokládat, že fungují stejně správně. Pokud však chceme být důslední, pak je možné relativně jednoduše „inkludovat“ tyto šablony testů do testů každé stránky. Testy jsou pak provedeny jako běžné ostatní okolní testy. Nejedná se ani o parametrizovatelné testy, ani o využití metody assertAll() či jejích modifikací.

  • test_utils – vše, co je využíváno ve vlastních testech (opět bude podrobně popsáno v části o JUnit5)

Obsahuje třídy:

  • ActiveTestSupport – abstraktní třída, která provádí nastavení parametrů jednotlivých testů a následně jejich vyhodnocení
  • Check – třída pro porovnání aktuálních a očekávaných hodnot; všechny asserty jsou pouze zde
  • Fail – metody pro jednotné reportování chybných výsledků
  • Tags – štítky testů; pomocí nich bude možné při externím spouštění testů vytvářet různé skupiny testů podle jejich zaměření, např. SMOKE
  • TestSettings – pomocí mechanismu rozšíření z JUnit5 je na začátku jakéhokoliv testu (jednorázově) načtena celá konfigurace a inicializován příslušný seleniový driver; na konci všech testů (opět jednorázově) driver ukončen a je provedeno závěrečné logování
  • TestEvaluation – framework JUnit5 poskytuje TestWatcher, pomocí něhož lze provést akce v případě, že test projde, selže nebo je ignorován; prakticky to znamená různé typy logovacích zápisů; opět se v testech aktivuje pomocí mechanismu rozšíření
  • tools – nástroje, které jsou testy využívány; všechny nástroje jsou aktivovány při načítání konfigurace

Obsahuje třídy:

  • Drivers – aktivace seleniových driverů pro jednotlivé konkrétní webové prohlížeče
  • ProjectLogger – logování
  • ScreenShot – snímání obrazovek

Typy funkcionálních testů

V podstatě jsou typově stejné, jako byly v předchozí verzi (viz Automatické testování webových aplikací: funkcionální testování podrobně ). Jejich kód je však díky využití balíků pageobject a services kratší a elegantnější.

Stručný přehled:

1. passive

Testy zjišťující, zda je na statické webové stránce vše nastaveno tak, jak je očekáváno; žádný z těchto testů nemění stav testované aplikace.

Po průchodu všech pasivních testů si můžeme být jisti, že počáteční stav aplikace odpovídá požadavkům / specifikaci. Pokrytí testy je zde 100%.

2. 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.

Poznámka:
Zde bych chtěl ještě jednou zdůraznit, že se nejedná o žádné bezpečnostní testy. Jsou to funkcionální testy očekávané reakce na chybný uživatelský vstup podle specifikace.

3. active

Testy, kdy je měněn stav testované aplikace.

Stoprocentní pokrytí aktivit je pouze v případech jedné jediné změny v jednom testu. To znamená například, že student si odzapíše předmět (jedna jednorázová akce), což se projeví následně i zrušením případných termínů zkoušky u tohoto studenta, a také u příslušného vyučujícího v seznamu studentů na předmětu. Otestovány jsou všechny existující změny včetně hraničních hodnot (student si odzapíše svůj jediný předmět).

Poznámka:
Po provedení jedné změny je tato změna otestována pomocí metod services.XY_Check u dotyčného studenta a dotyčného vyučujícího. Při tom se předpokládá, že dříve provedené pasivní testy s dostatečnou mírou jistoty potvrzují, že „co je v databázi, to se i zobrazí“. Pokud bychom však chtěli být důslední a mít jistotu správné funkcionality a správného zobrazení, pak by po každé jednotlivé změně měly proběhnout opakovaně všechny pasivní testy. Ty by např. zjistily chybu typu, kdy si student odzpsal předmět a tentýž předmět byl odzapsán i nějakému jinému studentovi.
Zjistíme-li (viz níže), jak dlouho trvají (asi 13 min), byl by tento postup sice zdlouhavý, ovšem nikoliv nerealistický. V dané struktuře testů a s existujícím balíkem support by bylo vcelku jednoduché tuto úpravu (na jednom místě ve třídě ActiveTestSupport) provést.

V sadě testů je pro ukázku jeden testovací scénář s několika v čase na sobě závisejícími změnami. Scénářům ale není věnována další pozornost, protože jejich zápis není příliš elegantní. Předpokládá se, že toto testování bude důkladně provedeno pomocí nadstavbového nástroje RobotFramework.

Rozsahy kódu

Pro celkovou představu o náročnosti testování je vhodné uvést rozsahy testů v porovnání s rozsahem vlastní aplikace. Údaje byly získány pomocí pluginu Statistic v IntelliJ.

Tabulka zobrazuje následující informace:

  • počet souborů – počet souborů, převážně javovských, v případě UIS také .jsp
  • velikost souborů – velikost souborů v kilobytech
  • počet řádků celkem – počet všech řádků, včetně prázdných řádků, komentářů apod.
  • počet řádků kódu – počet řádků, které obsahují kód

Aplikace UIS je samostatně otestována jednotkovými testy (řádka „JUnit UIS“), které nespadají do popisovaného projektu funkcionálních testů.

Řádka „funkcionální“ představuje údaje o celém zde popisovaném projektu, tj. balíky funcional_tests + support

Řádka „poměr všechny testy/UIS“ udává poměry všech testů vůči výkonnému kódu testované aplikace UIS

počet souborů

velikost souborů [kB]

počet řádků celkem

počet řádků kódu

Aplikace UIS

Java

87

340

8550

4708

JSP

18

94

1550

1477

dohromady:

105

434

10100

6185

Testy

JUnit UIS

33

277

6945

4690

JUnit funkcionální

30

97

2649

2309

funkcionální support

101

452

14707

7763

funkcionální testy

81

248

7530

5829

dohromady:

245

1024

31831

20591

Poměry

všechny testy / UIS

2,33

2,36

3,15

3,33

funkcionální testy / UIS

1,73

1,61

2,20

2,20

Budeme-li tedy považovat za nejpřesnější metriku počet řádků kódu (LOC – Lines of Code), pak všechny typy testů mají dohromady více než trojnásobný rozsah oproti velikosti kódu aplikace.

Omezíme-li se pouze na popisovaný projekt funkcionálních testů (a vynecháme-li z něj jednotkové testy), pak je rozsah stále více než dvojnásobný.

Co se týče odhadu pracnosti – celý projekt trval zhruba jeden člověkoměsíc, tj. cca 170 až 200 hodin.

Počty testů

Dalšími zajímavými údaji mohou být informace o počtech testů v jednotlivých skupinách a doby jejich trvání.

počet testů

počet assertů

doba trvání [sec]

passive

890

2702

780

active

64

2351

1477

negative

29

52

50

dohromady:

983

5105

2307

Pokus vás zarazil nepoměr počtu assertů a doby jejich trvání, pak je vysvětlení jednoduché. U pasivních testů se testy provádějí vždy na jedné zobrazené stránce, kdežto u aktivních testů se pro každý test zobrazuje jiná stránka, případně se přihlašuje jiný uživatel (přepnutí ze studenta na učitele a naopak).

Závěr

I tato verze projektu testování potvrdila, že komplexní automatizované testování je drahé. Je však realizovatelné a v případě výše popisované struktury i snadno spravovatelné a rozšiřitelné.

Na závěr předchozí minisérie byla uvedena i rozsáhlejší úvaha o tom, zda se taková aktivita v praxi vyplatí. Závěry z úvahy z mého pohledu platí stále a v případě zájmu si je můžete přečíst v Automatické testování webových aplikací: funkcionální testování podrobně 

Pro jistotu ještě jednou opakuji, že popisovaný projekt je v podstatě případ funkcionálního testování dotaženého do extrému. A pro běžné projekty (které nejsou bezpečnostně kritické) by taková míra protestovatelnosti byla zřejmě zbytečným luxusem.

Pokud vás z této problematiky něco více zaujalo a potřebovali byste podrobnější informace o tomto projektu, je možné se domluvit na osobní konzultaci nebo i na nějaké formě školení.