Viktor Terinek
Test Consultant
Blog
Viktor Terinek
Test Consultant
Leckdo dnes sdílí zkušenosti s automatizací testů. I vzhledem k tomu, že jsem první skript vyprodukoval před cca 15 lety, jsem si řekl, že je na čase přispět i svou 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:
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 nástupu zaměstnance do práce.
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“ tedy 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 (více se dočtete například v článku POM design pattern).
Testy musely dále 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:
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).
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říklad při každé změně kódu vývojářem.
Jak bylo zmíněno výše, GUI testy jsou implementovány na platformě Robot Framework 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, respektive ukončujících, direktiv (Intro, resp. Closure ) jen ze seznamu obrazovek. Například v test casu Symptom 1 | Below 40| worsening klíčové slovo pass 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 |
view rawresult-1-03.robot hosted with ❤ by GitHub
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 |
view rawscreensLib.robot hosted with ❤ by GitHub
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} |
view rawcommonLib.robot hosted with ❤ by GitHub
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 |
view rawsaveResults.feature hosted with ❤ by GitHub
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') |
view rawE2EhappyPathProcess.feature hosted with ❤ by GitHub
Volání testů z repository je realizováno pomocí:
call read('../ResponsiblePerson/validateRToken.feature')
Když chceme nějaký test volat v sekci Background, která v Cucumberu obsahuje kroky platné pro každé Scenario, použijeme:
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. Detailněji viz dokumentace.
Obecně lze k dokumentaci Karate konstatovat, že je na skvělé úrovni, obzvlášť vezmeme-li v úvahu, že jde o open source.
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 |
view rawsendResultSlack.feature hosted with ❤ by GitHub
1. 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, pokud jsou správně vybrané a používané, dokáží reálně zvýšit celkovou efektivitu týmu.
2. API testy
Framework Karate se snažím šířit mezi testery a aplikovat na projektech už nějaký ten pátek, důkazem budiž moje 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í.
Pokud vás napadá otázka, proč Karate a proč ne RestAssured, zkuste třeba toto poměrně komplexní srovnání.
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ě, protože 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říklad klíčová slova, která vybírají položku ze seznamu, použít prakticky nelze.
Další problém je komplexnost aplikace v JS jako taková. Často 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 držet se při zemi s výběrem klíčových slov pro simulaci akcí uživatele. Osvědčilo se mi:
click element Input Text
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áznakem ř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.