Les 6: Testing

Sebastiaan HenauOngeveer 18 minuten

Les 6: Testing

In deze les ligt de focus niet op het schrijven van React code, maar op het gebruik van Cypressopen in new window, een tool waarmee we end-to-end en component tests kunnen schrijven. Cypress is niet afhankelijk van een bepaald JavaScript framework, wat hier besproken wordt, werkt dus ook met frameworks zoals Angular, Vue, Svelte, ...

We bespreken in deze les zowel component tests als end-to-end (E2E) tests. De focus zal echter liggen op de E2E tests aangezien dit interessanter is en relevanter voor de oefeningen en voorbeelden die we gemaakt en besproken hebben doorheen het semester. Voor dit voorbeeld vertrekken we van een licht aangepaste versie van het voorbeeld uit les 5.

Startbestandenopen in new window

Waarschuwing

Voor deze les gaan we ervan uit dat je de absolute basis van TypeScript kent, af en toe zullen we TypeScript syntax moeten gebruiken om geen foutmeldingen te krijgen in onze tests. Als je nog nooit gebruik gemaakt hebt van TypeScript, raden we je aan om de introductie in de cursus van mobiele applicatiesopen in new window te lezen. Het is niet nodig dat je de oefeningen in deze cursus gemaakt hebt, enkel dat je een ruw idee hebt van TypeScript.

End-to-end versus component test

Cypress biedt zoals reeds vermeld ondersteuning voor E2E en component test. Voor de meeste applicaties zijn end-to-end test interessanter omdat deze je volledige applicatie testen, zoals een gebruiker het zou doen. Je test dus niet enkel de front-end code, maar ook de interactie met de back-end en de database. End-to-end testen kunnen geschreven worden door volledig gebruik te maken van de UI en de daaraan gekoppelde back-end functies, of je kan ervoor kiezen om de requests naar de back-end te onderscheppen en vervolgens een fixture (vooraf gedefinieerde testdata) terug te geven. Daarnaast kan je de (test) database ook seeden met bepaalde voorgedefinieerde data en dit eventueel voor/na elke test doen.

Component tests daarentegen testen slechts een klein deel van de applicatie, enkel de component die getest wordt (en eventuele kinderen) komen aan bod. Een component test is bijgevolg enkel interessant voor componenten die veel herbruikt zullen worden doorheen verschillende delen van een applicatie of voor componenten die deel uitmaken van een componenten library zoals React-Bootstrapopen in new window, MUIopen in new window, Semantic UIopen in new window of Next UIopen in new window.

Installatie van Cypress

Het grootste deel van de configuratie van Cypress gebeurd automatisch. We beginnen met de cypress en cyress-vite libraries te installeren. Merk op dat we deze libraries installeren als development dependency, het is tenslotte niet nodig dat een gebruiker alle Cypress code download om je website te bekijken. Cypress is enkel relevant tijdens het ontwikkelen van je code en in CI/CD pipelinesopen in new window.

pnpm add -D cypress cypress-vite

Nadat deze libraries geïnstalleerd zijn kunnen we Cypress openen en de moeilijkere configuratie door Cypress laten doen.

pnpm exec cypress open 
Figuur 1: Cypress installatie zonder configuratie

Op bovenstaand scherm kan je een test soort installeren, vervolgens zal Cypress de nodige files toevoegen.

Figuur 2: Cypress configureren

Mappenstructuur

Een standaard Cypress installatie voegt heel wat extra mappen toe aan je project. De verschillende mappen worden hieronder besproken. In onderstaand voorbeeld is Cypress geïnstalleerd in een React project gemaakt met Vite, enkel de mappen en relevante bestanden worden vermeld, de overige Vite bestanden zijn natuurlijk nog steeds aanwezig. Naast de basisbestanden die hierboven gegenereerd zijn, zijn er ook enkele extra bestanden vermeld die we nog zullen toevoegen.

/
 |- cypress
 |  |- e2e
 |  |   |- userStory.cy.js
 |  |- fixtures
 |  |- screenshots
 |  |- support
 |  |   |- commands.js
 |  |   |- component.js
 |  |   |- component-index.html
 |  |   |- e2e.js
 |  |   |- index.ts
 |  |- videos
 |- public
 |- src
 |- cypress.config.js
 |- tsconfig.json

 
 
 
 
 
 
 
 
 
 
 
 
 


 

/cypress.config.js

Deze configuratiefile kan gebruik om heel wat instellingen te overschrijven, we bespreken in deze les enkel de instellingen die relevant zijn voor hetgeen we willen bereiken. Je kan de documentatieopen in new window raadplegen voor een volledige lijst.

Ten eerste willen we gebruikmaken van Vite net zoals in onze React projecten, we hebben hiervoor reeds een plug-in geïnstalleerd, we moeten deze enkel nog activeren. Dankzij deze plug-in hebben we niet enkel een snellere applicatie, maar kunnen we ook gebruik maken van dezelfde environment variable variabelen als in het project.

Verder voegen we ook de URL van de dev server toe aan de configuratiefile, zo moeten we tijdens de tests niet telkens de volledige URL intypen.

Tenslotte voegen we ook enkele nieuwe environment variables toe die specifiek zijn voor de tests. Deze variabelen zullen gebruikt worden om een account aan te maken waarmee voor de tests. Let op, vervang in het mailadres de tekst [JOUW NAAM HIER] met een eigen naam. Het is belangrijk dat je onderstaande structuur hanteert. De back-end staat enkel toe om testing account te verwijderen zonder authenticatie, er wordt gecontroleerd op onderstaande structuur.

WebStorm klaagt in onderstaande code dat de functie defineConfig niet gevonden kan worden, je mag deze foutmelding negeren. WebStorm vergist zich en deze import is wel degelijk correct.

import {defineConfig} from 'cypress'
import vitePreprocessor from 'cypress-vite'

export default defineConfig({
    e2e: {
        setupNodeEvents(on, config) {
            on('file:preprocessor', vitePreprocessor())
        },
        baseUrl: 'http://localhost:5173',
    },

    component: {
        devServer: {
            framework: 'react',
            bundler: 'vite',
        },
    },
    
    env: {
        supabaseFunctionsUrl: 'https://vaolhgulafwfxgrqpngy.functions.supabase.co/',
        testAccount: 'cypressAccount[JOUW NAAM HIER]@testing.com',
        testPassword: 'test123test',
        testUsername: 'Test Account',
        authCookie: 'sb-vaolhgulafwfxgrqpngy-auth-token'
    }
})

 
 




 

 









 
 
 
 
 
 
 


/cypress/e2e

Deze map bestaat nog niet maar zal aangemaakt worden als we een eerste e2e test schrijven. De map bevat natuurlijk de end-to-end tests. De naam van een e2e test moet het formaat [Naam].cy.js hebben. Wat je voor de extensie .cy.js zet is niet heel belangrijk. Je kan bijvoorbeeld de naam van je user stories gebruiken als het project via de Agile methodologie gebouwd wordt.

/cypress/fixtures

Deze map bevat data die gebruikt kan worden om de database te initialiseren of om terug te geven na een HTTP request. We zullen in deze cursus geen gebruik maken van fixturesopen in new window.

/cypress/screenshots & /cypress/videos

Deze mappen bevatten, zoals de naam doet vermoeden, screenshots en videos. Deze worden standaard genomen als een test mislukt en als deze uitgevoerd wordt in de terminal in de plaats van via de GUI. Dit is vooral handing in CI/CD pipelines. Je voegt deze mappen best toe aan je .gitignore.

/tsconfig.json

Dit bestand is nodig om intellisenseopen in new window ondersteuning te bieden en om geen foutmeldingen te krijgen in de editor. Dit bestand wordt niet automatisch aangemaakt.

{
  "compilerOptions": {
    "lib": [
      "es2015",
      "dom"
    ],
    "allowJs": true,
    "noEmit": true,
    "types": [
      "cypress",
      "./cypress/support",
      "node"
    ]
  },
  "include": [
    "**/*.*"
  ]
}

/cypress/support

Deze map bevat bestanden die ondersteuning bieden voor je eigenlijke testen. We zullen deze map bijvoorbeeld gebruiken om commando's te schrijven waarmee we een test account kunnen aanmaken, verwijderen. Deze commando's kunnen vervolgens gebruikt worden in de eigenlijke tests.

/cypress/support/component-index.html

Dit bestand wordt gebruikt al basis van elke component test. De component die getest wordt, wordt in dit bestand gemount als root element. Het bestand heeft dus een vergelijkbaar doel als index.html in onze React projecten.

/cypress/support/commands.js

Deze file bevat algemene commands die zowel voor end-to-end tests als component tests gebruikt worden.

/cypress/support/e2e.js

Deze file bevat configuratie en commando's die enkel relevant zijn voor end-to-end tests.

/cypress/support/components.js

Deze file bevat commando's en andere configuratie die enkel relevant zijn voor component tests.

/cypress/support/index.ts

Dit bestand wordt niet standaard aangemaakt, het is opnieuw vereist om intellisense te activeren en geen foutmeldingen te genereren. Voorlopig ziet het bestand er als volgt uit. Later zullen we zelf een aantal Cypress commando's schrijven en dan zullen we dit bestand verder uitbreiden zodat intellisense werkt en er geen foutmeldingen getoond worden in WebStorm.

declare global {
    namespace Cypress {
        interface Chainable<ISubject = any> {
        }
    }
}

Component tests

In dit voorbeeld schrijven we een test voor de errorMessage component die in /src/utils/errorMessage.jsx te vinden is in de startbestanden. We testen deze component zonder rekening te houden met de rest van de applicatie, dit betekent dat alle properties meegegeven worden in de test en geen enkele property uit de state van bovenliggende componenten komt.

We beginnen met een nieuwe file aan te maken in dezelfde map als de ErrorMessage component, dat de files in dezelfde map staan is niet vereist, maar is wel aan te raden. De naam van een component test heeft volgende structuur: [component naam].cy.jsx. Voor dit voorbeeld wordt de naam van de test file dus errorMessage.cy.jsx.

Test structuur

Elke Cypress test begint met de describeopen in new window functie, binnen deze functie worden verschillende itopen in new window calls gebruikt om een specifieke test te definiëren.

Begrip: describe & it

De describeopen in new window functie neemt als eerste argument een string die de naam voor een groep testen definieert. Als tweede argument wordt een arrow functie meegegeven. Binnen deze arrow functie wordt de itopen in new window functie gebruikt om een specifieke test aan te duiden. De signatuur van deze laatste functie is volledig gelijk aan die van de describe functie. De hoeveelheid it calls is niet gelimiteerd, daarbovenop is het mogelijk om describe binnen een describe te gebruiken.

describe('Een groep testen', () => {
    it('Should do something when some condition is true', () => {
        // Test code
    })

    it('Should do something else when some other condition is true', () => {
        // Test code
    })
})

Test: Component kan gemount worden

De eerste test die we schrijven is heel eenvoudig, we testen enkel of de component al dan niet gemount kan worden. Met andere woorden, we testen of de component geen syntax errors bevat en dat deze geldige JSX-code teruggeeft. We gebruiken de mount functie die door Cypress voorzien wordt om onze component te mounten in component-index.html.

import ErrorMessage from './errorMessage.jsx'
import { mount } from 'cypress/react18'

describe('<ErrorMessage>', () => {
    it('Mounts', () => {
        mount(<ErrorMessage/>)
    })
})

Als we Cypress openen zou de nieuwe file gedetecteerd moeten zijn en kunnen we onze eerste test uitvoeren.

Figuur 3: Een eerste test

Commando's definiëren

In bovenstaande code hebben we de mount functie moeten importeren, dit zal nodig zijn voor elke component test die we schrijven. In de plaats van deze actie telkens te herhalen kunnen we een custom commandopen in new window schrijven dat de import voor ons afhandelt. Zo'n command kan in alle test files gebruikt worden zonder dat dit expliciet geïmporteerd moet worden.

Omdat het commando enkel interessant is voor component tests en niet voor end-to-end tests plaatsen we dit commando in component.js.

import './commands'
import { mount } from 'cypress/react18'

Cypress.Commands.add('mount', mount)

 

 

Bovenstaande code geeft nog een foutmelding, dit is niet omdat de code niet correct is, maar omdat we Cypress gebruiken in een JavaScript project in de plaats van in een TypeScript project. We kunnen dit probleem oplossen door het index.ts bestand aan te passen dat we eerder toegevoegd hebben. Als we hierin aangeven dat het mount commando bestaat, worden de foutmeldingen niet langer getoond.

declare global {
    namespace Cypress {
        interface Chainable<ISubject = any> {
            /**
             * Mount a React component.
             * @example
             * cy.mount(<></>)
             */
            mount(dataAttribute: ReactElement): Chainable<MountReturn>
        }
    }
}








 



Nu het commando gedefinieerd is, kunnen we dit gebruiken in onze test, hier is geen import statement voor nodig. Als alles goed geconfigureerd is, zou WebStorm ook intellisense moeten aanbieden voor het nieuwe commando.

import ErrorMessage from './errorMessage.jsx'

describe('<ErrorMessage>', () => {
    it('Mounts', () => {
        cy.mount(<ErrorMessage/>)
    })
})





 



Test: Kinderen worden getoond wanneer deze gedefinieerd zijn

De component die we testen is zeer eenvoudig, als er kinderen zijn worden deze getoond in een Alertopen in new window die de danger kleur krijgt. Als er geen kinderen zijn, wordt er niets getoond.

We moeten dus controleren of er al dan niet iets gerenderd wordt. De eerste stap is natuurlijk weer het renderen van de ErrorMessage component, dit kan op exact dezelfde manier als hierboven. Vervolgens moeten we een DOM referentie naar de gerenderde foutboodschap ophalen. Als we de Bootstrap documentatieopen in new window raadplegen zien we dat de alert CSS-klasse gebruikt wordt in de definitie van een alert. We kunnen dan een CSS-selectoropen in new window schrijven waarmee we elk element met de CSS-klasse alert ophalen. Dit kan via het cy.get commando.

Begrip: cy.get

Het getopen in new window commando kan gebruikt worden om een bepaald element uit de DOM op te halen, het argument van dit commando is een CSS-selectoropen in new window.

describe('Get demo', () => {
    it('Retrieves the element with the class demo', () => {
        cy.get('.demo')
    })
})

Dit is natuurlijk niet voldoende, enkel een referentie ophalen is niet voldoen. We moeten ook controleren of dit element echt bestaat. Dit kan via de shouldopen in new window methode. De mogelijke argumenten van deze editor zijn heel uitgebreid. De volledige lijst is te vinden op in de cypress documentatieopen in new window. Hieronder gebruiken we deze functie om te controleren of de alert gerenderd is of niet. Daarnaast controleren we ook of dat wat gerenderd wordt de tekst is die we hebben meegegeven tijdens het renderen.

import ErrorMessage from './errorMessage.jsx'

describe('<ErrorMessage>', () => {
    it('Mounts', () => {
        cy.mount(<ErrorMessage/>)
    })
    
    it(`Shows the children when they are defined`, () => {
        cy.mount(<ErrorMessage>A child text</ErrorMessage>)
        cy.get('.alert').should('exist').should('have.text', 'A child text')
    })
})










 



Onderstaan screenshot toont dat de tests slagen, maar dat de Bootstrap opmaak nog niet zichtbaar is in Cypress, alhoewel dit niet cruciaal is, is het dikwijls wel handiger als deze opmaak aanwezig is. Het is zo eenvoudiger om te herkennen waar het fout loopt en verder te debuggen in je effectieve applicatie.

Figuur 4: Geen opmaak

We kunnen natuurlijk in elke component een import statement plaatsen voor Bootstrap, maar dit is niet heel efficient. Een beter oplossing is de component.ts file die we ook gebruikt hebben om het mount commando te definiëren. Zo wordt het CSS-bestand ingeladen voor elke test.

import './commands'
import {mount} from 'cypress/react18'
import 'bootstrap/dist/css/bootstrap.min.css'

Cypress.Commands.add('mount', mount)


 


CSS-selectors vs data attributen

In bovenstaand voorbeeld hebben we gebruik gemaakt van een CSS-klasse om een referentie naar de Alert component op te halen, dit zou een last-resort optie moeten zijn in de plaats van de standaard.

Alhoewel CSS-klassen en id's werken, zijn deze, heel sterk gebonden aan de styling. Als Bootstrap in de volgende versie beslist om de klassen die een alert definieert te hernoemen, of om bijvoorbeeld enkel de alert-danger klasse te gebruiken in de plaats van de alert alert-danger combinatie, hebben we een probleem. We kunnen niet upgraden naar de laatste Bootstrap versie zonder dat alle testen herschreven moeten worden. Langs de andere kant zouden we er ook voor kunnen kiezen om bijvoorbeeld een card te gebruiken om de foutmelding weer te geven in de plaats van een alert.

Om problemen te vermijden is het interessanter om specifieke properties voor testing te voorzien, in deze cursus spreken we af om steeds een property van de vorm data-cy te gebruiken. Data attributesopen in new window kunnen aan elk HTML-element toegevoegd worden, de enige voorwaarde is dat de naam begint met data-. Door zulke attributen te gebruiken zijn we niet meer afhankelijk van de opmaak. Als we wisselen naar een card moeten we enkel de naam van de component aanpassen, het attribuut blijft gewoon staan. We kunnen de test dan aanpassen zodat er gebruik gemaakt wordt van het nieuwe data attribuut.

const ErrorMessage = ({children}) => {
    if (children === null) {
        return <></>
    }

    return (
        <Alert variant="danger" data-cy="error-message">
            {children}
        </Alert>
    )
}






 




getByData commando

Bovenstaande code werkt, maar als we overal een selector van de vorm data-cy=... moeten doorgeven aan het get commando, wordt de code langer dan nodig. Aangezien we deze selector verschillende keren zullen gebruiken, is het interessant om er een nieuw commando voor te definiëren.

In tegenstelling tot het vorige commando is dit commando nuttig voor end-to-end en component tests, we definiëren het dus in commands.js in de plaats van in component.js.

Cypress.Commands.add('getByData', (selector) => {
    return cy.get(`[data-cy="${selector}"]`);
})

Test: Er wordt niets gerenderd als er geen kinderen zijn

We schrijven nog een laatste component test. Deze test moet controleren dat er niets gerenderd wordt als er geen kinderen meegegeven worden aan de ErrorMessage component.

import ErrorMessage from './errorMessage.jsx'

describe('<ErrorMessage>', () => {
    it('Mounts', () => {
        cy.mount(<ErrorMessage/>)
    })
    
    it(`Shows the children when they are defined`, () => {
        cy.mount(<ErrorMessage>A child text</ErrorMessage>)
        cy.getByData('error-message')
            .should('exist')
            .should('have.text', 'A child text')
    })
    
    it(`Doesn't show anything when the children are undefined`, () => {
        cy.mount(<ErrorMessage/>)
        cy.getByData('error-message').should('not.exist')
    })
})















 
 
 
 


Als we bovenstaande test uitvoeren, zien we duidelijk dat de test mislukt.

Figuur 5: Mislukte test

De test mislukt omdat de conditionele controlestructuur in de ErrorMessage component fout gebouwd is. Als we deze component aanpassen, slaagt de test.

const ErrorMessage = ({children}) => {
    if (!children) {
        return <></>
    }

    return (
        <Alert variant="danger" data-cy="error-message">
            {children}
        </Alert>
    )
}

 









End-to-end tests

End-to-end tests zijn complexer dan component tests. We moeten in elke test een specifieke pagina bezoeken, daarnaast moeten we ook rekening houden met gebruikersaccounts, HTTP requests, ...

We beginnen met de registreer- en inlogfunctionaliteit te testen. Daarna breiden schrijven we een test waarmee we nieuwe mappen kunnen aanmaken in het bestandssysteem. De startbestanden zijn reeds aangepast met data-cy attributen.

Om de juiste pagina te openen, moeten we gebruik maken van het visit commando, elke URL die we hier ingeven wordt, krijgt automatisch de baseUrl als prefix. Via de clickopen in new window methode kan op een DOM-element geklikt worden, we gebruiken dit hier om om de login knop te drukken. Vervolgens testen we of er al dan niet een success/error boodschap getoond wordt.

Test: Registreren

describe('Create an account', () => {

    it(`Can't submit an empty form`, () => {
        cy.visit('/login')
        cy.getByData('submit-btn').click()

        cy.getByData('submit-btn-loading').should('not.exist')
        cy.getByData('success-message').should('not.exist')
        cy.getByData('error-message').should('not.exist')
    })
})

Onderstaande video toont duidelijk dat de test mislukt. Daarnaast zie toont de video alle HTTP requests verstuurd worden. In dit geval zijn het er 22 omdat we een account proberen aan te maken als de logingegevens niet overeenkomen met een bestaande account. Verder zie je in de mislukte assert ook dat Cypress 40004000 milliseconden wacht voordat de test faalt. Dit is handig als het HTTP request traag is en het even duurt voordat het request verwerkt is en de succesboodschap getoond wordt.

We kunnen het probleem gemakkelijk oplossen door het required attribuut toe te voegen aan de formulier-elementen.

Figuur 6: Registreer test mislukking & fix

Test: Een wachtwoord moet minstens 6 karakters lang zijn

Deze test begint op dezelfde manier als de vorige, maar we geven een email adres en wachtwoord (van 44 karakters) in. Hiervoor kunnen we gebruik maken van de typeopen in new window methode. Tenslotte testen we of de loading animatie getoond wordt op de submit knop en dat de boodschap met de foutmelding getoond wordt. De success boodschap mag natuurlijk niet zichtbaar zijn. Merk op dat we tijdens deze test gebruik maken van de Cypress environment variables (die gedefinieerd zijn in cypress-config.js).

describe('Create an account', () => {
    
    it(`Can't submit an empty form`, () => {
        cy.visit('/login')
        cy.getByData('submit-btn').click()

        cy.getByData('submit-btn-loading').should('not.exist')
        cy.getByData('success-message').should('not.exist')
        cy.getByData('error-message').should('not.exist')
    })

    it(`Can't submit a password with fewer then 6 characters`, () => {
        cy.visit('/login')
        cy.getByData('email').type(Cypress.env('testAccount'))
        cy.getByData('password').type('11111')
        cy.getByData('submit-btn').click()

        cy.getByData('submit-btn-loading').should('exist')
        cy.getByData('error-message').should('exist')
        cy.getByData('success-message').should('not.exist')
    })
})

Tests optimaliseren

We zien in de twee testen die we tot nu toe geschreven hebben al relatief veel duplicatie. We moeten voor beide testen de /login pagina bezoeken. Daarnaast hebben we voor beiden ook een referentie naar de submit knop nodig.

Concept: beforeEach

De beforeEachopen in new window hook wordt uitgevoerd voor elke test die in het describe blok staat. Alle algemene code, die voor elke test nodig is, kan hierin geplaats worden.

describe('Some test group', () => {
    beforeEach(() => {
        // Common code for each test.
    })
    
    it('Some test', () => {
    })

    it('Some other test', () => {
    })
})

Natuurlijk is het niet voldoende om de cy.getByData calls te verhuizen naar de beforeEach functie. We moeten hier terug naar kunnen refereren. Dit kan door middel van aliassen.

Concept: Alias

Een alias kan toegevoegd worden via het asopen in new window commando. Dit commando kan gekoppeld worden aan een HTML-element, JSON-data, HTTP-requests, ...

https://docs.cypress.io/guides/core-concepts/variables-and-aliases#Aliases

De beforeEachopen in new window hook wordt uitgevoerd voor elke test die in het describe blok staat. Alle algemene code, die voor elke test nodig is, kan hierin geplaatst worden. We kunnen de alias vervolgens gebruiken via het getopen in new window commando.

describe('Some test group', () => {
    beforeEach(() => {
        cy.getByData('some-data-attribute').as('alias')
    })
    
    it('Some test', () => {
        cy.get('@alias').click()
    })
})

We kunnen de tests dan herschrijven met een beforeEach, merk op dat de success en error boodschappen nog niet gerenderd zijn tot er op submit knop gedrukt wordt. We kunnen deze dus niet in de beforeEach plaatsen.

describe('Create an account', () => {
    beforeEach(() => {
        cy.visit('/login')
        cy.getByData('submit-btn').as('submit')
        cy.getByData('email').as('email')
        cy.getByData('password').as('password')
    })
    
    it(`Can't submit an empty form`, () => {
        cy.get('@submit').click()

        cy.getByData('submit-btn-loading').should('not.exist')
        cy.getByData('success-message').should('not.exist')
        cy.getByData('error-message').should('not.exist')
    })

    it(`Can't submit a password with fewer then 6 characters`, () => {
        cy.get('@email').type(Cypress.env('testAccount'))
        cy.get('@password').type('11111')
        cy.get('@submit').click()

        cy.getByData('submit-btn-loading').should('exist')
        cy.getByData('error-message').should('exist')
        cy.getByData('success-message').should('not.exist')
    })
})

 
 
 
 
 
 


 
 







 
 
 





Test: Account aanmaken

Om een account aan te maken, moeten we het formulier indienen met geldige data. Maar we moeten ook garanderen dat de account effectief aangemaakt wordt, i.e. dat we niet inloggen op een bestaande account. We moeten er dus voor zorgen dat de test account niet bestaat voordat deze aangemaakt wordt in de test. Om dit te testen hebben we een extra custom command nodig dat gebruikt kan worden om de account te verwijderen.

We gebruiken volgende commando's om een account aan te maken en te verwijderen en in te loggen.

Examen

Je moet niet in staat zijn de code voor deze commando's te reproduceren voor het examen, als je tijdens het examen een test account moet aanmaken of verwijderen of gebruiken, worden de commando's gegeven. Je moet deze enkel gebruiken.

We verwijzen de geïnteresseerde lezer door naar de documentatie:

Info

Nadat onderstaande code toegevoegd is aan het project, moet je Cypress herstarten.

// Adapted from https://github.com/supabase/supabase/discussions/6177
import {createClient} from '@supabase/supabase-js'

let supabase

export const getCurrentSession = async ({email, password, supabaseURL, supabaseKey}) => {
    // If there's already a supabase client, use it, don't create a new one.
    if (!supabase) {
        supabase = createClient(supabaseURL, supabaseKey)
    }

    // Create a session for the user.
    const {data, error} = await supabase.auth.signInWithPassword({
        email,
        password,
    })

    return {...data, error}
}

We kunnen bovenstaande commando's vervolgens gebruiken om voor de start en na het einde van de registreer test de test account te verwijderen. Als deze niets bestaat doet het commando niets. Merk op dat we de enter toets indrukken nadat het wachtwoord ingetypt is, voor een volledige lijst van de mogelijke toetsen die op deze manier ingegeven kunnen worden, verwijzen we door naar de documentatieopen in new window. Daarnaast maken we ook gebruikt van het urlopen in new window commando om de URL uit te lezen.

describe('Create an account', () => {
    beforeEach(() => {
        cy.visit('/login')
        cy.getByData('submit-btn').as('submit')
        cy.getByData('email').as('email')
        cy.getByData('password').as('password')
    })
    
    it(`Can't submit an empty form`, () => {
        // Niet relevante code weggelaten
    })

    it(`Can't submit a password with fewer then 6 characters`, () => {
        // Niet relevante code weggelaten
    })
    
    it('Creates an account and is redirected to the username page', () => {
        cy.deleteTestAccount()
        cy.get('@email').type(Cypress.env('testAccount'))
        cy.get('@password').type(`${Cypress.env('testPassword')}{enter}`)

        cy.url().should('contain', '/login/username')
        cy.deleteTestAccount()
    })
})

















 

 

 
 


Gevaar: Trage tests

Maak enkel in je registreer/inlog test een account aan via de UI. Voor alle andere testen is dit overbodig. Je weet dat inloggen en registreren werkt, je kan voor alle andere tests dus gebruik maken van HTTP Requests om een account aan te maken of te verwijderen. Dit gaat veel sneller dan via de UI.

Verder is het ook mogelijk om deze HTTP requests te onderscheppen en zelf data terug te geven in de plaats van via de server te werken. Dit is nog sneller, maar dan wordt de code die de server aanspreekt natuurlijk niet getest. We verwijzen de geïnteresseerde lezer door naar de Cypress documentatieopen in new window.

Test: Username kiezen

We schrijven vervolgens tests uit die het username form testen. Merk op dat we in onderstaande code gebruik maken van het locationopen in new window command om het path op te halen, dit is niet hetzelfde als de URL. De URL bevat het protocol (http/https), de domeinnaam, en de poort. Het path is enkel het laatste stuk, het stuk dat na de poort komt.

We gebruiker ook de before en after hooks, deze worden respectievelijk uitgevoerd voor en na alle tests in het describe blok.

describe(`Choose a username`, () => {
    
    before(() => {
        cy.createTestAccount(false)
        cy.login()
    })
    
    beforeEach(() => {
        cy.visit('/')
        cy.getByData('submit-btn').as('submit')
        cy.getByData('username').as('username')
    })

    after(() => {
        cy.deleteTestAccount()
    })

    it('Is shown the username page when logged in and no username has been chosen', () => {
        cy.url().should('contain', '/login/username')
    })

    it(`Can't submit the form with an empty username`, () => {
        cy.get('@submit').click()
        cy.getByData('success-message').should('not.exist')
        cy.getByData('error-message').should('not.exist')
    })

    it(`Enters a valid username and is redirected to the home page and can't visit the homepage again`, () => {
        cy.get('@username').type(`${Cypress.env('testUsername')}{enter}`)
        cy.getByData('success-message').should('exist')
        cy.getByData('error-message').should('not.exist')
        cy.location('pathname').should('equal', '/')
    })
})


































Onderstaande video toont dat het overgrote deel van de testen mislukt of niet uitgevoerd wordt. De eerste test lukt, de gebruiker is ingelogd en wordt automatisch doorgestuurd naar het username formulier.

Figuur 7: Username tests mislukken

Het probleem is dat de gebruiker uitgelogd wordt na de eerste test, Cypress verwijderd alle cookies en data in localstorage na elke test. Dit is vervelend tijdens het schrijven van testen, maar heeft wel een goede reden. Elke test moet volledig onafhankelijk zijn van elke andere test. Het login commando moet dus verhuist worden naar de beforeEach hook. Daarnaast is het ook beter wanneer geen enkele test op een andere steunt, dus verplaatsen we de create en delete account commando's ook naar beforeEach en afterEach hooks.

describe(`Choose a username`, () => {

    beforeEach(() => {
        cy.createTestAccount(false)
        cy.login()
        cy.visit('/')
        cy.getByData('submit-btn').as('submit')
        cy.getByData('username').as('username')
    })

    afterEach(() => {
        cy.deleteTestAccount()
    })

    it('Is shown the username page when logged in and no username has been chosen', () => {
        cy.url().should('contain', '/login/username')
    })

    it(`Can't submit the form with an empty username`, () => {
        cy.get('@submit').click()
        cy.getByData('success-message').should('not.exist')
        cy.getByData('error-message').should('not.exist')
    })

    it(`Enters a valid username and is redirected to the home page and can't visit the homepage again`, () => {
        cy.get('@username').type(`${Cypress.env('testUsername')}{enter}`)
        cy.getByData('success-message').should('exist')
        cy.getByData('error-message').should('not.exist')
        cy.location('pathname').should('equal', '/')
    })
})



 







 



















Na deze aanpassing slagen alle testen.

Figuur 8: Username tests slagen

Test: Folder aanmaken

We beginnen met een test te schrijven waarmee we controleren of een folder succesvol aangemaakt is. Hiervoor hebben we het totaal aantal folders voor en na het wijzigen nodig.

Het getopen in new window commando geeft één of meer matches terug, we kunnen het aantal matches opvragen via het itsopen in new window commando. Dit commando geeft een bepaalde property uit het voorgaande object terug. Alle commando kettingen in Cypress zijn asynchroon. We kunnen het resultaat dus niet zomaar in een variabele plaatsen, ook async/await is niet mogelijk. We moeten het thenopen in new window commando gebruiken. Alhoewel de structuur gelijk is aan de then methode van een Promise, zijn deze twee dingen niet hetzelfde.

const folderName = 'A new folder'

describe('Create a Folder', () => {

    beforeEach(() => {
        cy.createTestAccount()
        cy.login()
        cy.visit('/filesystem')
        cy.getByData('new-folder').as('newFolder')
        cy.getByData('folder').as('folders')
    })

    afterEach(() => {
        cy.deleteTestAccount()
    })

    it('Clicks the new folder button and created a new folder', () => {
        cy.get('@folders').its('length').then(oldLength => {
            cy.get('@newFolder').click()
            cy.get('input').type(`${folderName}{enter}`)
            cy.get('@folders')
                .should('have.length', oldLength + 1)
                .should('contain.text', folderName)
        })
    })
})
 


















 
 
 
 
 
 
 

In de volgende test, testen we op de navigatie doorheen het bestandssysteem. Hiervoor hebben we de vorige knop nodig, in de plaats van een data-cy attribuut kunnen we ook de contains methode gebruiken om binnen een lijst van gematchte DOM-elementen een specifiek element op te halen dat een specifieke tekst bevat.

const folderName = 'A new folder'

describe('Create a Folder', () => {

    beforeEach(() => {
        cy.createTestAccount()
        cy.login()
        cy.visit('/filesystem')
        cy.getByData('new-folder').as('newFolder')
        cy.getByData('folder').as('folders')
    })

    afterEach(() => {
        cy.deleteTestAccount()
    })

    it('Clicks the new folder button and created a new folder', () => {
        cy.get('@folders').its('length').then(oldLength => {
            cy.get('@newFolder').click()
            cy.get('input').type(`${folderName}{enter}`)
            cy.get('@folders')
                .should('have.length', oldLength + 1)
                .should('contain.text', folderName)
        })
    })

    it(`Opens a default folder, creates a subfolder,  navigates to it and back to the top`, () => {
        cy.get('@folders').contains('React').click()
        cy.get('@newFolder').click()
        cy.get('input').type(`${folderName}{enter}`)
        cy.get('@folders').should('contain.text', folderName)
        cy.get('@folders').contains('..').click()
        cy.get('@folders').should('not.contain.text', folderName)
    })

    it(`Checks that a users folders aren't visible to unauthenticated users`, () => {
        cy.get('@newFolder').click()
        cy.get('input').type(`${folderName}{enter}`)
        cy.get('@folders').should('contain.text', folderName)

        cy.get('body').contains('Log out').click()
        cy.get('@folders').should('not.contain.text', folderName)
    })
})








































 



Voorbeeldcode

Volledig uitgewerkte lesvoorbeelden met commentaaropen in new window

Laatst geüpdate:
Bijdragers: Sebastiaan Henau