Do you also want to be part of Tesena?
Case study
Appka Covid-19: case study automatizace testů
Leckdo dnes sdílí své zkušenosti s automatizací testů. I vzhledem k tomu, že první skript jsem vyprodukoval před cca 15ti lety, jsem si řekl, že je na čase přispět i mou troškou do mlýna. Žádný strach, nepůjde o prehistorický příběh nostalgicky vzpomínající na muzejní technologie, ale o zkušenost z jara 2020. V článku najdete:
- Ukázku implementace GUI testů v Robot Framework
- Příklad implementace API testů v Karate
- Inspiraci na optimální architekturu automatizovaných testů
- Pro a proti vybraných technologií na automatizaci
- Tip na řízení agilního projektu dle metodiky Scrumban
Charakter projektu
Zadání
Agilní tým standardní velikosti (tj. cca 8 lidí) dostal za úkol vyvinout aplikaci, která pomáhá urychlit proces ověření zdravotní způsobilosti pracovníka pro vstup na pracoviště. Typický scénář je následující: Brzo ráno přijde zaměstnanci SMS s odkazem, pod kterým se schovává dotazník indikující možnost nakažení virem Covid-19. Před vstupem na pracoviště pověřený vrátný zkontroluje na svém mobilu, že přicházející pracovník vyplnil dotazník, a změří mu teplotu, kterou též vloží do aplikace. Pakliže aplikace nevyhodnotí riziko nákazy, zaměstnanec je šťastný, že může pracovat, a je vpuštěn na pracoviště.
Design aplikace je primárně přizpůsoben mobilním telefonům. Dotazník ve formě wizardu předhazuje uživateli obrazovky s jednotlivými dotazy: podle toho, jak uživatel odpoví na jedné obrazovce, ho směruje na další obrazovku.
Použité technologie a nástroje
V rámci analýzy byly použity nástroje pro grafický popis obrazovek Figma a na diagramy draw.io, ve kterém byly zachycen rozhodovací strom pro směrování uživatele mezi obrazovkami.
Řízení projektu probíhalo v Jira Kanban projektu, procesně pak dle Scrumban metodiky. Oblast testů pokrýval Jira plugin TM4J. Veškerý kód byl samozřejmě uložen repozitářích s podporou verzování (GitLab).
Scrumban, jak název napovídá, je kříženec metodologií Scrum a Kanban s cílem více rozvolnit Scrum uzamknutý do dnes již pro některé týmy příliš rigidních sprintů. Další informace můžete získat v samostatném článku o Scrumban metodologii.
Frontend aplikace je vytvořen v javascriptovém frameworku React, který přes REST rozhraní volá služby nad backendem v Javě a menší persistencí v PostgreSQL.
K automatizaci GUI testů byla použita tradiční kombinace Robot Framework s knihovnou Selenium, resp. Karate na API testy.
Karate Framework je toho času jediný open-source nástroj kombinující automatizaci API testů, mockování a zátěžové testování do jednoho unifikovaného frameworku. Pro zápis testů používá syntaxi Gherkin, která je jakožto BDD ideální i pro ne-programátory. Krom efektivních JSON&XML validací nabízí např. i paralelní spouštění testů. Home page projektu najdete zde.
Testovací strategie
Cest, kterými uživatel mohl být směrován mezi obrazovkami, bylo mnoho (neboli rozhodovací strom aplikace byl velmi košatý). Od začátku bylo tedy nabíledni, že si nevystačíme s manuálním testováním. Automatizované GUI testy dostaly za úkol pokrýt všechny možné varianty průchodu rozhodovacím stromem a být vyvíjeny společně s aplikací.
Servisně orientovaná aplikace vyzývala k použití automatizovaných API testů, takže souběžně s dokončováním jednotlivých API, byly implementovány i API testy. Zároveň bylo cílem pokrývat skrze volání API celý proces, tj. simulovat kompletní use case, kdy frontend aplikace nasbírá od uživatele odpovědi z dotazníku a odesílá je přes API k dalšímu zprocesování, které končí ne/doporučením, zda může zaměstnanec do práce.
Architektura testů
GUI testy
Testy byly navrženy se zřetelem na jejich snadnou udržovatelnost a flexibilitu při pozdějších změnách. Dnes stále ještě populární technika “Record&Replay” tudíž nebyla vůbec použita. Aplikace organizovaná do jednotlivých obrazovek indikovala použití návrhového vzoru POM.
Page Object Model (POM) je návrhový vzor velmi rozšířený v oblasti automatizace testů. Výrazně přispívá k efektivitě údržby testů především díky zamezení duplikací v kódu (viz např. článek Page Object Model design pattern).
Dále testy musely splňovat velmi snadnou a rychlou tvorbu v mnoha variantách průchodů rozhodovacím stromem, jednak z důvodu zmíněné flexibility a také z důvodu vytváření test casů (test)analytikem bez technických znalostí nutných ke scriptování.
Celé řešení tak sestávalo ze tří vrstev:
- Test Cases: průchody rozhodovacím stromem, tj. obrazovky dotazníku s vyplněnými daty a seřazenými v očekávaném sledu
- Screens: implementace všech obrazovek, tj. logika jejich vyplňování.
- Shared support libraries: podpůrné technické komponenty, usnadňující implementaci obrazovek (naskriptování určitých sdílených UI prvků, nezbytná podpůrná komunikace přímo na API, atp.) a umožňující specifické konfigurace testů (typ browseru, headless mode pro spouštění v CI, atp.)
Krom benefitů implikovaných POM přístupem umožnila tato koncepce i další podstatnou výhodu: test design testovacích případů mohl vznikat prakticky nezávisle na implementaci obrazovek. Stačilo znát jejich název a strukturu dat, což bylo definováno už v rámci návrhu designu obrazovek (viz Figma výše).
API testy
V případě API byla situace jednodušší. Každé API bylo pokryto elementárním testem. Tyto elementární testy byly dále použity i pro E2E scénáře pokrývající několik málo happy day use casů.
Hlavním účelem API testů bylo prověřit celkovou kondici aplikace, aby mohla být testována, někdy se takové testy označují termínem “Health check”. Neboli bezproblémový průběh API testů byl kritériem pro spouštění GUI testů.
Výhodou API testů bylo i to, že exekuce kompletní testovací sady byla velmi rychlá (v řádu sekund), čili nebyl problém API testy spouštět velmi často, např. při každé změně kódu vývojářem.
Design a implementace testů
GUI testy
Implementace GUI testů je postavena na platformě RobotFramework podporované knihovnami SeleniumLibrary a REST.
Test cases: každý jeden test case reprezentuje jeden průchod rozhodovacím stromem aplikace. Typický test case tak sestává krom zahajovacích, resp. ukončujících, direktiv (Intro, resp. Closure ) jen ze seznamu obrazovek. Např. v test casu Symptom 1 | Below 40| worsening
klíčové slovopass symptoms fever
říká: “Na obrazovce s otázkami na symptomy vyber, že máš horečku, a pokračuj dále”.
*** Settings ***
Resource __TSSetup.robot
Metadata Path result-1-03
*** Test Cases ***
Symptom 1 | Below 40 | worsening
[Tags] Smoke
Intro
Pass symptoms fever
Pass serious-fever LOW
Pass getting-worse YES
Pass result-1
Closure
Symptom 2 | worsening
Intro
Pass symptoms cough
Pass getting-worse YES
Pass result-1
Closure
Screens: obrazovky jsou uloženy v repository (jedno místo v adresářové struktuře projektu).
*** Settings ***
Documentation Screens objects definition
Resource screens/Intro.robot
Resource screens/Closure.robot
Resource screens/results.robot
*** Variables ***
${SCREEN_BUTTON} //button[@id="nextButton"]
*** Keywords ***
# Screen components repository
Component YesNo
# answer: YES|NO
[Arguments] ${answer}
click enabled element //button[@type="submit" and @value="${answer}"]
# Screens repository
Pass symptoms
# symptomNames: fever cough shortnessOfBreath
[Arguments] @{symptomNames}
Wait Until Page Contains 2/5
Location Should Contain /symptoms
FOR ${symptomName} IN @{symptomNames}
click enabled element name:symptoms.${symptomName}
END
click enabled element ${SCREEN_BUTTON}
Pass serious-fever
# fever: LOW|HIGH
[Arguments] ${fever}
Location Should Contain /serious-fever
click enabled element //input[@name="seriousFever" and @value="${fever}"]
click enabled element ${SCREEN_BUTTON}
Pass getting-worse
# answer: YES|NO
[Arguments] ${answer}
Location Should Contain /getting-worse
Component YesNo ${answer}
Pass result-1
Wait Until Page Contains 5/5
sleep ${SLEEP_ELEMENT}
Location Should Contain /result-1
Shared support libraries: sdílené technické komponenty jsou umístěny samostatně. Jde typicky o různé “vychytávky”, které si přinášíme jako zkušenosti/tipy z jiných projektů, ale i o pomůcky specifické pro daný projekt.
*** Keywords ***
click enabled element
[Arguments] ${element}
Wait Until Element Is Enabled ${element}
# sleep just for debugging or demo purposes
sleep ${SLEEP_ELEMENT}
click element ${element}
Move AgeSlider Up
[Arguments] ${age}
${ageInt} = Convert To Integer ${age}
${steps} = Set Variable ${ageInt - 50}
FOR ${index} IN RANGE ${steps}
click enabled element //form/div[1]/div[2]/div[2]
END
Move AgeSlider Down
[Arguments] ${age}
${ageInt} = Convert To Integer ${age}
${steps} = Set Variable ${ageInt - 50}
FOR ${index} IN RANGE ${steps.__abs__()}
click enabled element //form/div[1]/div[2]/div[1]
END
Intro
[Arguments] ${token}=${screeningToken}
Init Browser ${ENDPOINT_TEST}${token}
click enabled element ${SCREEN_BUTTON}
Location Should Contain terms-of-service
click enabled element name:termsOfServiceConsent
click enabled element ${SCREEN_BUTTON}
API testy
Implementace API testů stojí na platformě Karate používající klíčová slova ve stylu Cucumber. Příklad izolovaného testu konkrétního API je triviální:
Feature: Employee clicked link
Scenario: Link opened by the employee from sms
* url BaseURL
Given path '/api/screening/' + SToken + '/result'
Given request {bloodyCough:#(bloodyCough),breathShortness:#(breathShortness),chills:#(chills),cough:#(cough),diarrhea:#(diarrhea),fastBreathing:#(fastBreathing),fatigue:#(fatigue),fever:#(fever),googleAnalyticsId:#(googleAnalyticsId),headache:#(headache),musclePain:#(musclePain),nausea:#(nausea),rapidWorsening:#(rapidWorsening),resultCode:"#(resultCode)",soreThroat:#(soreThroat),suspiciousContact:#(suspiciousContact),temperature:0,time:#(time),traveledCountry:#(traveledCountry),userCookieId:#(userCookieId)}
When method put
Then status 200
I v API testech byl záměr přepoužívat elementární testy umístěné v jedné repository. Uložení dat dotazníku z příkladu výše tak najdete i v příkladu E2E průchodu procesem:
Feature: We Are Safe tests
Background:
* def BaseURL = UrlBase
* def healthResults = read('healthResults.json')
* def Tokens = callonce read('../generateData.feature')
* def SToken = Tokens.response.screeningToken
* def RToken = Tokens.response.responsiblePersonToken
* def check2Abort = (Tokens.response == 'failed' ? karate.abort() : {} )
Scenario: E2E test happy path
* def validateSToken = call read('../CoronaChecker/validateSToken.feature')
* match validateSToken.response contains {result:'PASSED',screeningId:#(SToken),valid:true}
* def saveResults = call read('../CoronaChecker/saveResults.feature') healthResults
* match saveResults.response == {id:#(SToken)}
* def validateSToken2 = call read('../CoronaChecker/validateSToken.feature')
* match validateSToken2.response contains {result:'FINISHED',screeningId:#(SToken),valid:true}
And eval if (validateSToken.responseStatus != 200) karate.call('sendResultSlack.feature')
Scenario: Validate token of the responsible person
* def validateRToken = call read('../ResponsiblePerson/validateRToken.feature')
Scenario: Read results by responsible person
* def readResults = call read('../ResponsiblePerson/readResults.feature')
* match readResults.response contains healthResults
And eval if (readResults.responseStatus != 200) karate.call('sendResultSlack.feature')
* def check2Abort = (readResults.responseStatus != 200 ? karate.abort() : {} )
Scenario: Add temperature by responsible person
* def measurementNumber = "1"
* def measuredTemperature = 39.0
* def addTemperature = call read('../ResponsiblePerson/addTemperature.feature')
* match addTemperature.response contains any {temperatureFirst:#(measuredTemperature)}
Scenario: Wait 7min
* def sleepTime = measurementWait
* def sleep7min = call read('../sleep.feature')
Scenario: Add temperature2 by responsible person
* def measurementNumber = "2"
* def measuredTemperature = 37.0
* def addTemperature2 = call read('../ResponsiblePerson/addTemperature.feature')
* match addTemperature2.response contains any {temperatureSecond:#(measuredTemperature)}
Scenario: Add transport by responsible person
* def transportType = "WALK"
* def addTransport = call read('../ResponsiblePerson/addTransport.feature')
* match addTransport.response == {id:#(SToken)}
Scenario: Check employee in list
* def getEmployees = call read('../ResponsiblePerson/getEmployees.feature')
* print getEmployees.response
* match getEmployees.response contains {id:#(SToken),temperatureSecond:#null,temperatureSecondTimestamp:#null,temperatureFirstTimestamp:#null,name:#ignore,resultCode:#null,temperatureFirst:#null,employeeId:1,id:#ignore,state:#ignore,temperatureFirst:#null,employeeId:1,id:#ignore,state:#ignore,finishWaitingTimestamp:#ignore}
And eval if (getEmployees.responseStatus == 400) karate.call('sendResultSlack.feature')
Volání testů z repository je realizováno pomocí:
call read('../ResponsiblePerson/validateRToken.feature')
call read('../ResponsiblePerson/validateRToken.feature')
Pakliže chceme nějaký test volat v sekci Background, která v Cucumberu obsahuje kroky platné pro každé Scenario, použijeme:
callonce read('../generateData.feature')
callonce read('../generateData.feature')
Operace callonce
zajistí, že reálné volání proběhne jen napoprvé a výsledek se uloží do cache pro další volání v rámci ostatních Scenarios ve Feature.
V kódu příkladu stojí za povšimnutí i kód realizující integraci na Slack:
And eval if (validateSToken.responseStatus != 200) karate.call('sendResultSlack.feature')
Feature: Slack integration
Scenario: Send Slack notification
* url 'https://hooks.slack.com/services/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'
Given request {"text": "vysledky testu najdete zde: https://we-are-safe.gitlab.io/testing/integration-tests/"}
When method POST
Then status 200
Projects Highlights & Downfalls
Co bylo fajn
- Být součástí vyladěného týmu
Nástroje a metodologie mohou být super, ale vždy se nakonec ukáže stará pravda, že vše je o lidech. Pakliže se sejde tým podobně motivovaných lidí, které práce baví, jsou profesionály (rozumí své práci a dokážou se správně zeptat, když něco nechápou), pracují pro tým (snaží se pomáhat, efektivně komunikují, je jim cizí “nechat ho v tom vykoupat”), je to moc hezký zážitek. A teprve pak ony nástroje a metodologie, pakliže jsou správně vybrané a používané, dokáží reálně zvýšit celkovou efektivitu týmu. - API testy
Framework Karate se snažím šířit mezi testery a aplikovat na projektech již nějaký ten pátek, důkazem budiž moje již přes rok stará propagace na TestStacku ;] Krom technických předností je zajímavý svým konceptem žádného GUI. Nakonec zjistíte, že klikání a vyplňování boxů v dnes populárních nástrojích (Postman, Insomnia, soapUI, apod.) prostě jenom zdržuje. Aplikace napsaná čistě v kódu navíc umožňuje používat verzovací systém (v našem případě osvědčený GIT), tzn. usnadní flexibilně spolupracovat v testerském tým, transparentně sdílet testy (typicky do vývojářského týmu), efektivně a spolehlivě se napojit do CI/CD orchestrací.
Pakliže vás napadá otázka, proč Karate a proč ne RestAssured, zkuste třeba toto poměrně komplexní srovnání.
Co trochu zabolelo
Hlavní nepříjemné překvapení (a asi jediné) z projektu byla bezzubost knihovny Selenium pro RobotFramework vůči JS framework React.
Selenium pro RF obsahuje řadu praktických klíčových slov simulujících akce na webové stránce. Musí se však používat obezřetně, neboť React a JS frameworky obecně vycházejí primárně z elementárních akcí uživatele: stisk klávesy a mouse click (případně mouse down/up). Takže např. klíčová slova, která vybírají položku ze seznamu, použít prakticky nelze.
Další problém je komplexnost aplikace v JS jako taková, leckdy podle vizuálních komponent nepoznáme, zda aplikace na pozadí ještě něco neprocesuje (např. nějaké asynchronní ajax volání).
Aby testy byly stabilní, doporučuji hlavně se držet při zemi s výběrem klíčových slov pro simulaci akcí uživatele. Tudíž osvědčilo se mi:
- vystačit si s několika málo klíčovými slovy na simulaci interakcí uživatele
click element
Input Text
- a též nepodceňovat kontroly, že daná obrazovka/komponenta je ready (s těmi doporučuji rozhodně nešetřit)
Wait Until Element Is Enabled
Wait Until Element Is Not Visible
Wait Until Page Contains
Location Should Contain
A veškerou komplikovanější logiku zapouzdřovat do uživatelsky definovaných klíčových slov (user keywords).
Náznak řešení je projekt robotframework-react, bohužel však nepříliš aktivní. Zatím je implementováno jediné klíčové slovo indikující, že aplikace je cele nastartovaná.
Toto téma by si však zasloužilo samostatný článek ;]