27. Nov 2023
FrontendEnd-to-end testovanie
Naším cieľom v GoodRequeste je tvoriť zmysluplný a zodpovedný digitálny svet. Z tohto dôvodu sme sa už dlhodobo zamýšľali ako vylepšiť procesy testovania, zvýšiť efektivitu testovania v prípade väčších zmien v aplikácii, zaručiť funkčnosť kritických častí aplikácie, aby sme našim klientom doručili robustný a spoľahlivý softvérový produkt.
Aké sú dôvody a príčiny, prečo vývojári neimplementujú end-to-end testy?
Existuje mnoho dôvodov, prečo vývojári neimplementujú end-to-end testy, ale je dôležité mať na pamäti, že testovanie je kľúčovou súčasťou vývoja softvéru. End-to-end testy sú zvyčajne nákladné na implementáciu a udržiavanie, a niekedy je náročné ich napísať tak, aby boli spoľahlivé a efektívne.
Medzi dôvody a príčiny, prečo vývojári neimplementujú E2E testy, patrí:
- Nedostatok času: Vývojári často čelia nedostatku času počas vývoja a sústredia sa na implementáciu funkcionality namiesto testovania.
- Nedostatok zdrojov: Nie vždy majú tímy na testovanie dostatočný počet testovacích zdrojov, ako sú ľudské zdroje alebo finančné prostriedky na automatizáciu testovania.
- Komplexita testov: E2E testy môžu byť náročné na implementáciu a údržbu, pretože vyžadujú testovanie integrovaného systému, nie len jednotlivých častí. Problémom je aj celkové nastavenie testovacieho prostredia a samotné spúšťanie a vyhodnocovanie testov.
- Orientácia na iné typy testov: Vývojári sa môžu zamerať na iné druhy testov, ako sú napríklad unit testy alebo integračné testy, ktoré sú jednoduchšie na implementáciu a poskytujú rýchlejšiu spätnú väzbu.
- Nedostatočná motivácia: Ak nie je kladený dôraz na kvalitu softvéru alebo ak chýba povedomie o výhodách testovania, môže to viesť k nedostatku motivácie na implementáciu e2e testov.
Aj keď existujú tieto dôvody, je dôležité si uvedomiť, že testovanie je kľúčové pre zabezpečenie kvality softvéru a minimalizáciu chýb. Implementácia E2E testov môže viesť k robustnejším a spoľahlivejším softvérovým produktom, čo by malo byť cieľom každého vývojára.
Prečo sme zvolili Cypress?
V rámci tímu sme mali prvú skúsenosť s nástrojom Selenium, s ktorým je Cypress často porovnávaný. Samotné nastavenie a implementácia niektorých scenárov bola pomerne komplikovaná, a preto sme sa rozhodli skúsiť aj Cypress. Pri výbere medzi Selenium a Cypress je dôležité zvážiť potreby projektu a zručnosti tímu. Selenium je univerzálny a škálovateľný, zatiaľ čo Cypress je optimalizovaný pre moderné webové aplikácie s jednoduchším a rýchlejším vývojom testov, a práve preto sme si zvolili Cypress, ktorý viac vyhovuje naším potrebám.
Viac o Cypresse nájdeš na ich blogu. Bližšie informácie k architektúre a kľúčovým vlastnostiam Cypressu nájdeš v článku: Key differences.
Organizácia testov
Po úspešnej inštalácii Cypressu do projektu sa vytvorí nasledujúca súborová štruktúra. Viac informácii o štruktúre a jednotlivých zložkách nájdeš v tomto článku: Writing and organizing tests.
Konfigurácia Cypressu
Naše odporúčania v prípade konfigurácie jednotlivých možností:
- V prípade, ak pozorujete veľkú spotrebu pamäte počas vykonávania testov, je možné nastaviť option numTestsKeptInMemory na 0, čo ju optimalizuje.
- V prípade optimalizácie a šetrenia zdrojov počas vykonávania testov sa nám osvedčilo vypnutie kompresie videí. → videoCompression: false.
- Občas sa stáva, že test počas vykonávania padne z neznámeho dôvodu. Preto odporúčame využiť option retries, ktorá zabezpečí opätovné spustenie testu a jeho možné úspešné vykonanie. Pri tomto probléme sa nám osvedčila optimálna hodnota 3. Bližšie informácie nájdeš v článku: Test retries.
- Niektoré testy môžu padnúť z dôvodu, že Cypress “nevyberie” element v časovom limite z dôvodu animácie alebo asynchrónnej operácie, ktorá blokuje zobrazenie elementu. V tomto prípade je potrebné navýšiť predvolenú hodnotu defaultCommandTimeout na 6000.
Viac informácií o základných a ďalších možnostiach sa môžeš dočítať v článku: Configuration.
Náš konfiguračný súbor
import { defineConfig } from 'cypress'
export default defineConfig({
projectId: '<projectID>',
scrollBehavior: 'center',
viewportWidth: 1920,
viewportHeight: 1080,
retries: 3,
numTestsKeptInMemory: 10,
videoCompression: false,
screenshotOnRunFailure: false,
e2e: {
setupNodeEvents: (on, config) => {
require('cypress-localstorage-commands/plugin')(on, config)
require('./cypress/plugins/index.ts').default(on, config)
require('@cypress/code-coverage/task')(on, config)
on('before:browser:launch', (browser, launchOptions) => {
// Chrome is used by default for test:CI script
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-dev-shm-usage')
} else if (browser.name === 'electron') {
launchOptions.args['disable-dev-shm-usage'] = true
}
return launchOptions
})
return config
},
env: {
auth_email: process.env.AUTH_EMAIL,
auth_password: process.env.AUTH_PASSWORD,
sign_in_url: process.env.SIGN_IN_URL
},
experimentalRunAllSpecs: true,
baseUrl: 'http://localhost:3001',
defaultCommandTimeout: 6000
},
})
Príklady test suitu a test casu
TestSuite → describe, context | TestCase → it, specify
Bližšie informácie nájdeš v článku: Writing tests.
describe('Users crud operations', () => {
it('Create user', () => {
...
})
it('Show user', () => {
...
})
it('Update user', () => {
...
})
it('Delete user', () => {
...
})
})
// alebo
context('Users crud operations', () => {
specify('Create user', () => {
...
})
specify('Show user', () => {
...
})
specify('Update user', () => {
...
})
specify('Delete user', () => {
...
})
})
Naming convention of folders
Tento model pomenovania a hierarchie priečinkov/testov (viď. na obrázku) sa nám osvedčil pri väčšom projekte, kde bolo potrebné implementovať veľké množstvo testov.
Hooks
V rámci nášho tímu využívame tieto hooky na autorizáciu a následne uloženie a obnovenie tokenov, tak aby nebolo potrebné sa prihlasovať pred každým testom, keďže testy sú izolované.
before(() => {
loginViaApi(email, password)
})
beforeEach(() => {
// restore local storage with tokens and salon id from snapshot
cy.restoreLocalStorage()
})
afterEach(() => {
// take snapshot of local storage with new refresh and access token
cy.saveLocalStorage()
})
Hooks, ktoré sú definované v zložke support
a súbore e2e.ts
sa automaticky spúšťajú pred každým test suitom. Tento spôsob môžete využiť pri prihlasovaní.
describe('Hooks', () => {
it('loginViaApi', () => {
cy.log(`sign_in_url is ${Cypress.env('sign_in_url')}`)
cy.log(`auth_email is ${Cypress.env('auth_email')}`)
cy.log(`auth_password is ${Cypress.env('auth_password')}`)
cy.apiAuth(Cypress.env('auth_email'), Cypress.env('auth_password'), Cypress.env('sign_in_url'))
})
})
Príklad funkcie, ktorú využívame na získanie tokenov potrebných na autorizáciu pri spúšťaní testov.
export const loginViaApi = (user?: string, password?: string) => {
cy.apiAuth(user || Cypress.env('auth_email'), password || Cypress.env('auth_password'), Cypress.env('sign_in_url'))
}
V hookoch je taktiež možné spúšťat príkazy, prípadne funkcie, na prípravu dát, ktoré sa očakávajú pri testoch. Pre viac informácií si prečítaj nasledujúci článok: Hooks.
Excluding and Including Tests
Viac o Excluding a Including testoch sa dozvieš v tomto článku: Excluding and Including tests.
Debugging
Pre viac informácií o debuggingu si prečítaj článok: Debugging.
Testing/data strategies
Pred vykonaním každého testu je nevyhnutné mať testovacie údaje uložené napríklad v databáze. Existuje mnoho spôsobov, ako pripraviť tieto údaje pred testovaním.
Viac informácií nájdeš: Testing strategies.
Seeding
Možnosti ako si pripraviť dáta potrebné pre jednotlivé testy
describe('Users CRUD operations', () => {
beforeEach(() => {
// pred každým testom môžete spustiť príkaz, ktorý premaže databázu
// a vytvorí nové záznamy
cy.exec('npm run db:reset && npm run db:seed')
// vytvorenie používateľa
cy.request('POST', '/user', {
body: {
// ... testovacie data
},
})
})
...
})
Viac informácií nájdeš v článku: Database initialization & seeding.
Stubbing
Príklad “odchytenia” post requestu.
{
...
cy.intercept('POST', '/user/*', {
statusCode: 200,
body: {
name: 'John Garfield'
}
}).as('createUser')
...
}
Pre viac informácií čítaj tu.
Best practices, tips and tricks
Selektovanie elementov - a.k.a. selectors
Príklad správneho identifikátora komponentu.
...
<div className={cx('input-inner-wrapper', { 'to-check-changes': toCheck })}>
<Input
{...input}
id={formFieldID(form, input.name)}
{/* alebo */}
data-cy={formFieldID(form, input.name)}
className={cx('input', { 'input-filter': fieldMode === FIELD_MODE.FILTER })}
onChange={onChange}
onBlur={onBlur}
addonBefore={addonBefore}
size={size || 'middle'}
onFocus={onFocus}
value={input.value}
/>
</div>
...
Príklad príkazu (“comandu”) na nastavenie hodnoty pre “pin” komponent. Ako vidieť nižšie na získanie elementu sa využíva unikátne ID, ktoré sa v našom prípade skladá z unikátneho názvu formulára a políčka.
Cypress.Commands.add('setValuesForPinField', (form: string, key: string, value: string) => {
const elementId: string = form ? `#${form}-${key}` : `#${key}`
const nthInput = (n: number) => `${elementId} > :nth-child(${n})`
const pin = [...value]
pin.forEach((char: string, index) =>
cy
.get(nthInput(index + 1))
.type(char)
.should('have.value', char)
)
})
Vytváranie vlastných príkazov (”comandov”)
Príklad ako využívame “custom commands” pri selektovaní a nastavovaní hodnôt jednotlivým elementom.
Cypress.Commands.add('selectOptionDropdownCustom', (form?: string, key?: string, value?: string, force?: boolean) => {
const elementId: string = form ? `#${form}-${key}` : `#${key}`
cy.get(elementId).click({ force })
if (value) {
// check for specific value in dropdown
cy.get('.ant-select-dropdown :not(.ant-select-dropdown-hidden)', { timeout: 10000 })
.should('be.visible')
.find('.ant-select-item-option')
.each((el: any) => {
if (el.text() === value) {
cy.wrap(el).click({ force })
}
})
} else {
// default select first option in list
cy.get('.ant-select-dropdown :not(.ant-select-dropdown-hidden)', { timeout: 10000 }).should('be.visible').find('.ant-select-item-option').first().click({ force: true })
}
})
Cypress.Commands.add('clickDropdownItem', (triggerId: string, dropdownItemId?: string, force?: boolean) => {
cy.get(triggerId).click({ force })
if (dropdownItemId) {
// check for specific value in dropdown
cy.get('.ant-dropdown :not(.ant-dropdown-hidden)', { timeout: 10000 })
.should('be.visible')
.find('.ant-dropdown-menu-item')
.each((el: any) => {
if (el.has(dropdownItemId)) {
cy.wrap(el).click({ force })
}
})
} else {
// default select first item in list
cy.get('.ant-dropdown :not(.ant-dropdown-hidden)', { timeout: 10000 }).should('be.visible').find('.ant-dropdown-menu-item').first().click({ force: true })
}
})
Viac informácií sa dozvieš v tomto článku: Custom commands.
Assigning Return Values
Namiesto definovania konštánt a uloženia vybraného elementu je potrebné využívať takzvané aliasy.
// DONT DO THIS. IT DOES NOT WORK
// THE WAY YOU THINK IT DOES.
const a = cy.get('a')
cy.visit('https://example.cypress.io')
// nope, fails
a.first().click()
// Instead, do this.
cy.get('a').as('links')
cy.get('@links').first().click()
Viac informácií nájdeš v článku: Variables and aliases.
Testovanie emailových notifikácií
Čo ak potrebujeme otestovať registráciu, ktorá zahŕňa potvrdenie emailu alebo aktivačný kód? Tak určite využite “task plugin” na načítanie emailu z mailovej schránky.
V Cypressi je možné použiť “task plugin” na vykonávanie “vlastných” úloh a úpravu chovania testovacieho prostredia. Tieto úlohy môžu zahŕňať rôzne operácie, ako sú prístup k súborom, manipulácia s dátami, volanie API, nastavovanie rôznych premenných a ďalšie.
V našom prípade používame “task” na načítanie emailov a získanie aktivačného kódu. Nižšie môžete vidieť príklad definovania tasku a následné využitie v teste.
- príklad “tasku” → ./plugins/index.ts
import axios from 'axios'
/**
* @type {Cypress.PluginConfig}
*/
export default (on: any, config: any) => {
on('task', {
getEmail(email: string) {
return new Promise((resolve, reject) => {
axios
.get(`http://localhost:1085/api/emails?to=${email}`)
.then((response) => {
if (response) {
resolve(response.data)
}
})
.catch((err) => {
reject(err)
})
})
}
})
return on
}
- príklad testu
it('Sign up', () => {
cy.intercept({
method: 'POST',
url: '/api/sign-up'
}).as('registration')
cy.visit('/')
cy.wait('@getConfig').then((interceptionGetConfig: any) => {
// check status code of login request
expect(interceptionGetConfig.response.statusCode).to.equal(200)
cy.clickButton(SIGNUP_BUTTON_ID, FORM.LOGIN)
// check redirect to signup page
cy.location('pathname').should('eq', '/signup')
cy.setInputValue(FORM.REGISTRATION, 'email', userEmail)
cy.setInputValue(FORM.REGISTRATION, 'password', user.create.password)
cy.setInputValue(FORM.REGISTRATION, 'phone', user.create.phone)
cy.clickButton('agreeGDPR', FORM.REGISTRATION, true)
cy.clickButton('marketing', FORM.REGISTRATION, true)
cy.clickButton(SUBMIT_BUTTON_ID, FORM.REGISTRATION)
cy.wait('@registration').then((interceptionRegistration: any) => {
// check status code of registration request
expect(interceptionRegistration.response.statusCode).to.equal(200)
// take local storage snapshot
cy.saveLocalStorage()
})
// check redirect to activation page
cy.location('pathname').should('eq', '/confirmation')
cy.task('getEmail', userEmail).then((email) => {
if (email && email.length > 0) {
const emailHtml = parse(email[0].html)
const htmlTag = emailHtml.querySelector('#confirmation-code')
if (htmlTag) {
cy.log('Confirmation code: ', htmlTag.text)
cy.visit('/confirmation')
cy.intercept({
method: 'POST',
url: '/api/confirmation'
}).as('activation')
cy.setValuesForPinField(FORM.ACTIVATION, 'code', htmlTag.text)
cy.clickButton(SUBMIT_BUTTON_ID, FORM.ACTIVATION)
cy.wait('@activation').then((interception: any) => {
// check status code of registration request
expect(interception.response.statusCode).to.equal(200)
// take local storage snapshot
cy.saveLocalStorage()
})
}
}
})
})
})
Viac informácií sa dozvieš v tomto článku: Task.
Code coverage
Na "inštrumentovanie” kódu používame knižnicu babel-plugin-istanbul a cypress/instrument-cra. V prípade knižnice cypress/instrument-cra je možné aktivovať inštrumentovaný kód aj v “dev móde” respektíve spustenia lokálneho dev servera.
{
"scripts": {
...
"start": "react-scripts -r @cypress/instrument-cra start",
...
}
}
Skript, ktorý používame na inštrumentovaný build aplikácie pre E2E testy.
{
"scripts": {
...
"build:coverage": "cross-env CYPRESS_INSTRUMENT_PRODUCTION=true NODE_ENV=production SKIP_PREFLIGHT_CHECK=true REACT_APP_VERSION=$npm_package_version react-scripts -r @cypress/instrument-cra build",
...
}
}
V package.json súbore je možné definovať, ktoré súbory v rámci “code basu” budú zahrnuté do “code coverage” reportu.
}
...
"nyc": {
"include": [
"src/pages/*"
],
"exclude": [
"src/pages/Calendar/**/*"
]
}
}
Knižnica @cypress/code-coverage slúži na integráciu → (spracovanie, aktualizáciu a uloženie) výsledkov “code coverage” počas vykonávania testov. Viac informácií nájdeš tu.
Test Automation and CI/CD Integration
Odporúčame si zvoliť takého CI poskytovateľa, ktorý má možnosť škálovať hardvérové zdroje (napr. Github actions a podobne…), nakoľko vykonávanie veľkého množstva testov, je náročná operácia.
V prípade, že nie je možné škálovať hardvérové zdroje alebo zmigrovať na iného CI poskytovateľa, tak ďalšou možnosťou je “postupné” spúšťanie testov. Takto je možné spúšťať špecifické testy pre rôzne role.
- Run specific test: Pre každé spustenie si vytvoríme skript do package.json súboru.
{
/* spustenie konrétneho test suitu */
"test:CI:auth": "AUTH_EMAIL=admin@test.com AUTH_PASSWORD=test SIGN_IN_URL=http://localhost:3000/api/login cypress run --spec cypress/e2e/01-users/auth.cy.ts",
/* spustenie všetkých test suitov, ktoré sa nachádzajú v priečinku "01-users" */
"test:CI:users": "AUTH_EMAIL=manager@test.com AUTH_PASSWORD=test SIGN_IN_URL=http://localhost:3000/api/login cypress run --spec cypress/e2e/01-users/*.ts",
}
- Cypress settings - env. variables: Do Cypress konfiguračného súboru je možné pridať všetky potrebné premenné, ktoré je možné následne používať v testoch.
...
env: {
auth_email: process.env.AUTH_EMAIL,
auth_password: process.env.AUTH_PASSWORD,
sign_in_url: process.env.SIGN_IN_URL
},
...
Integráciu je možné rozšíriť o možnosť Cypress cloud. Hlavnou výhodou je možnosť paralelizácie vykonávania testov a spravovanie výsledkov testov.
Viac informácií o integrácii nájdeš v článku: Running Cypress in continuous integration.