Blog

Viktor Terinek

Viktor Terinek

Test Consultant

Appka Covid-19: case study automatizace testů

Automatizace
Úspěšných příběhů o automatizaci testů není nikdy dost. Přečtěte si jeden o GUI a API testech, třeba právě v něm najdete chybějící inspiraci.

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:

  • 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

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 nástupu zaměstnance 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“ 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:

  1. 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
  2. Screens: implementace všech obrazovek, tj. logika jejich vyplňování
  3. 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říklad při každé změně kódu vývojářem.

Design a implementace testů

GUI testy

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

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 

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

Projects Highlights & Downfalls

Co bylo fajn

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

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ě, 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:

  • Vystačit si s několika málo klíčovými slovy na simulaci interakcí uživatele:
click element Input Text
  • Nepodceňovat kontroly toho, ž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á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.