Idee per configurare, organizzare e consolidare i tuoi test Cypress #frontend@twiliosendgrid

Pubblicato: 2020-12-15

Abbiamo scritto molti test Cypress su diverse applicazioni Web e team di frontend qui su Twilio SendGrid. Poiché i nostri test sono stati scalati su molte funzionalità e pagine, ci siamo imbattuti in alcune utili opzioni di configurazione e abbiamo sviluppato modi per mantenere meglio il nostro numero crescente di file, selettori per gli elementi della nostra pagina e valori di timeout lungo il percorso.

Il nostro obiettivo è mostrarti questi suggerimenti e idee per configurare, organizzare e consolidare le tue cose relative a Cypress, quindi sentiti libero di fare con loro ciò che funziona meglio per te e il tuo team.

Questo post sul blog presuppone che tu abbia una conoscenza pratica dei test Cypress e stai cercando idee per mantenere e migliorare meglio i tuoi test Cypress. Tuttavia, se sei curioso di saperne di più su funzioni, asserzioni e modelli comuni che potresti trovare utile per scrivere test Cypress in ambienti separati, puoi invece dare un'occhiata a questo post del blog di panoramica di mille piedi.

Altrimenti, andiamo avanti e ti guidiamo prima attraverso alcuni suggerimenti per la configurazione di Cypress.

Configurazione del tuo cypress.json

Il file cypress.json è dove puoi impostare tutta la configurazione per i tuoi test Cypress come i timeout di base per i tuoi comandi Cypress, variabili di ambiente e altre proprietà.

Ecco alcuni suggerimenti da provare nella tua configurazione:

  • Perfeziona i timeout dei comandi di base, in modo da non dover aggiungere sempre { timeout: timeoutInMs } nei comandi di Cypress. Armeggia con i numeri finché non trovi il giusto equilibrio per impostazioni come "defaultCommandTimeout", "requestTimeout" e "responseTimeout".
  • Se il tuo test prevede iframe, molto probabilmente dovrai impostare "chromeWebSecurity" su false in modo da poter accedere a iframe multiorigine nella tua applicazione.
  • Prova a bloccare gli script di marketing, analisi e registrazione di terze parti quando esegui i test Cypress per aumentare la velocità e non attivare eventi indesiderati. Puoi facilmente impostare un elenco di rifiuti con la loro proprietà "blacklistHosts" (o "blockHosts" in Cypress 5.0.0), prendendo una serie di glob di stringhe corrispondenti ai percorsi host di terze parti.
  • Regola le dimensioni della finestra di default con "viewportWidth" e "viewportHeight" per quando apri la GUI di Cypress per rendere le cose più facili da vedere.
  • Se ci sono problemi di memoria in Docker per test pesanti o se desideri contribuire a rendere le cose più efficienti, puoi provare a modificare "numTestsKeptInMemory" su un numero inferiore a quello predefinito e impostare "videoUploadOnPasses" su false per concentrarti sul caricamento di video per test non riusciti corre solo.

In un'altra nota, dopo aver modificato la configurazione di Cypress, puoi anche aggiungere TypeScript e digitare meglio i tuoi test Cypress come abbiamo fatto in questo post del blog. Ciò è particolarmente utile per il completamento automatico, avvisi di tipo o errori durante la chiamata e il concatenamento di funzioni (come cy.task('someTaskPluginFunction) , Cypress.env('someEnvVariable') o cy.customCommand() ).

Potresti anche voler provare a configurare un file cypress.json di base e caricare un file di configurazione Cypress separato per ogni ambiente di test, ad esempio uno staging.json quando esegui diversi script Cypress nel tuo package.json . La documentazione di Cypress fa un ottimo lavoro nel guidarti attraverso questo approccio.

Organizzare la tua cartella Cypress

Cypress imposta già una cartella cypress di primo livello con una struttura guidata quando avvii Cypress per la prima volta nella tua base di codice. Ciò include altre cartelle come l' integration per i file delle specifiche, i fixtures per i file di dati JSON, i plugins in per le cy.task(...) e altre configurazioni e una cartella di support per i comandi e i tipi personalizzati.

Una buona regola pratica da seguire quando si organizza all'interno delle cartelle Cypress, dei componenti React o di qualsiasi codice in generale è quella di mettere insieme le cose che potrebbero cambiare insieme. In questo caso, dal momento che abbiamo a che fare con le applicazioni Web nel browser, un approccio che si adatta bene è raggruppare le cose in una cartella per pagina o per funzionalità generale.

Abbiamo creato una cartella di pages separata partizionata in base al nome della funzione come "SenderAuthentication" per contenere tutti gli oggetti della pagina sotto il percorso /settings/sender_auth come "DomainAuthentication" (/settings/sender_auth/domains/**/*) e "LinkBranding" (/impostazioni/sender_auth/links/**/*). Nella nostra cartella dei plugins in, abbiamo anche fatto la stessa cosa con l'organizzazione di tutti i file dei plug-in cy.task(...) per una determinata funzione o pagina nella stessa cartella per l'autenticazione del mittente e abbiamo seguito lo stesso approccio per la nostra cartella di integration per i file delle specifiche Abbiamo ridimensionato i nostri test a centinaia di file di specifiche, oggetti pagina, plug-in e altri file in una delle nostre basi di codice, ed è facile e conveniente navigare.

Ecco uno schema di come abbiamo organizzato la cartella dei cypress .

Un'altra cosa da considerare quando si organizza la cartella di integration (dove risiedono tutte le specifiche) è potenzialmente dividere i file delle specifiche in base alla priorità del test. Ad esempio, potresti avere tutti i test con la priorità più alta e il valore in una cartella "P1" e i test con la seconda priorità in una cartella "P2" in modo da poter eseguire facilmente tutti i test "P1" impostando l'opzione --spec come --spec 'cypress/integration/P1/**/*' .

Crea una gerarchia di cartelle specifiche che funzioni per te in modo da poter raggruppare facilmente le specifiche non solo per pagina o funzionalità come --spec 'cypress/integration/SomePage/**/*' , ma anche in base ad altri criteri come priorità, prodotto , o ambiente.

Selettori di elementi consolidanti

Quando sviluppiamo i componenti React della nostra pagina, in genere aggiungiamo un certo livello di test unitari e di integrazione con Jest ed Enzyme. Verso la fine dello sviluppo delle funzionalità, aggiungiamo un altro livello di test Cypress E2E per assicurarci che tutto funzioni con il backend. Sia gli unit test per i nostri componenti React che gli oggetti pagina per i nostri test Cypress E2E richiedono selettori per i componenti/elementi DOM sulla pagina con cui desideriamo interagire e su cui asserire.

Quando aggiorniamo tali pagine e componenti, è possibile che si verifichino derive ed errori dovuti alla sincronizzazione di più posizioni dai selettori di unit test all'oggetto pagina Cypress al codice componente effettivo stesso. Se ci basassimo esclusivamente sui nomi delle classi relativi agli stili, sarebbe doloroso ricordare di aggiornare tutti i punti che potrebbero rompersi. Invece, aggiungiamo "data-hook", "data-testid" o qualsiasi altro attributo "data-*" denominato in modo coerente a componenti ed elementi specifici sulla pagina che desideriamo ricordare e scriviamo un selettore per esso nei nostri livelli di test.

Possiamo aggiungere attributi "data-hook" a molti dei nostri elementi, ma avevamo ancora bisogno di un modo per raggrupparli tutti in un unico posto per aggiornarli e riutilizzarli in altri file. Abbiamo escogitato un modo digitato per gestire tutti questi selettori "data-hook" da distribuire sui nostri componenti React e da utilizzare nei nostri test unitari e selettori di oggetti di pagina per un maggiore riutilizzo e una manutenzione più semplice negli oggetti esportati.

Per la cartella di primo livello di ogni pagina, creeremo un file hooks.ts che gestisce ed esporta un oggetto con un nome chiave leggibile per l'elemento e il selettore CSS di stringa effettivo per l'elemento come valore. Li chiameremo "Selettori di lettura" poiché dobbiamo leggere e utilizzare il modulo del selettore CSS per un elemento in chiamate come wrapper.find(“[data-hook='selector']”) di Enzyme nei nostri unit test o Cypress's cy.get(“[data-hook='selector']”) negli oggetti della nostra pagina. Queste chiamate sembrerebbero quindi più pulite come wrapper.find(Selectors.someElement) o cy.get(Selectors.someElement) .

Il seguente frammento di codice copre più del perché e come utilizziamo in pratica questi selettori di lettura.

Allo stesso modo, esportiamo anche oggetti con un nome chiave leggibile per l'elemento e con un oggetto come { “data-hook”: “selector” } come valore. Li chiameremo "Selettori di scrittura" poiché abbiamo bisogno di scrivere o diffondere questi oggetti su un componente React come oggetti di scena per aggiungere con successo l'attributo "data-hook" agli elementi sottostanti. L'obiettivo sarebbe fare qualcosa del genere e l'elemento DOM effettivo sottostante, supponendo che gli oggetti di scena siano passati correttamente agli elementi JSX, avrà anche l'attributo "data-hook=" impostato.

Il seguente frammento di codice copre più del perché e come utilizziamo in pratica questi selettori di scrittura.

Possiamo creare oggetti per consolidare i nostri selettori di lettura e scrivere selettori per aggiornare meno posizioni, ma se dovessimo scrivere molti selettori per alcune delle nostre pagine più complesse? Questo può essere più soggetto a errori da costruire da soli, quindi creiamo funzioni per generare facilmente questi selettori di lettura e selettori di scrittura da esportare eventualmente per una determinata pagina.

Per la funzione del generatore del selettore di lettura, eseguiremo il ciclo delle proprietà di un oggetto di input e formeremo la stringa del selettore CSS [data-hook=”selector”] per ciascun nome di elemento. Se il valore corrispondente di una chiave è null , assumeremo che il nome dell'elemento nell'oggetto di input sarà lo stesso del valore "data-hook" come { someElement: null } => { someElement: '[data-hook=”someElement”] } . Altrimenti, se il valore corrispondente di una chiave non è null, possiamo scegliere di sovrascrivere quel valore "data-hook" come { someElement: “newSelector” } => { someElement: '[data-hook=”newSelector”]' } .

Per la funzione di generazione del selettore di scrittura, scorreremo le proprietà di un oggetto di input e formeremo gli oggetti { “data-hook”: “selector” } per ogni nome di elemento. Se il valore corrispondente di una chiave è null , assumeremo che il nome dell'elemento nell'oggetto di input sarà lo stesso del valore "data-hook" come { someElement: null } => { someElement: { “data-hook”: “someElement” } } . Altrimenti, se il valore corrispondente di una chiave non è null , possiamo scegliere di sovrascrivere quel valore "data-hook" come { someElement: “newSelector” } => { someElement: { “data-hook”: “newSelector” }' } .

Usando queste due funzioni del generatore, costruiamo il nostro selettore di lettura e scriviamo oggetti selettore per una pagina e li esportiamo per essere riutilizzati nei nostri test unitari e oggetti pagina Cypress. Un altro vantaggio è che questi oggetti sono digitati in modo tale da non poter eseguire accidentalmente Selectors.unknownElement o WriteSelectors.unknownElement nei nostri file TypeScript. Prima di esportare i nostri selettori di lettura, consentiamo anche di aggiungere elementi extra e mappature dei selettori CSS per componenti di terze parti su cui non abbiamo il controllo. In alcuni casi, non possiamo aggiungere un attributo "data-hook" a determinati elementi, quindi dobbiamo ancora selezionare in base ad altri attributi, ID e classi e aggiungere più proprietà all'oggetto selettore di lettura come mostrato di seguito.

Questo schema ci aiuta a rimanere organizzati con tutti i nostri selettori per una pagina e per quando dobbiamo aggiornare le cose. Ti consigliamo di studiare i modi per gestire tutti questi selettori in una sorta di oggetto o con qualsiasi altro mezzo per ridurre al minimo il numero di file che devi toccare per modifiche future.

Consolidamento dei timeout

Una cosa che abbiamo notato dopo aver scritto molti test Cypress è che i nostri selettori per determinati elementi si interrompevano e richiedevano più tempo dei valori di timeout predefiniti impostati nel nostro cypress.json come "defaultCommandTimeout" e "responseTimeout". Tornavamo indietro e correggevamo alcune pagine che richiedevano timeout più lunghi, ma poi nel tempo il numero di valori di timeout arbitrari è cresciuto e mantenerlo è diventato più difficile per modifiche su larga scala.

Di conseguenza, abbiamo consolidato i nostri timeout in un oggetto che inizia al di sopra del nostro "defaultCommandTimeout", che è compreso tra cinque e dieci secondi per coprire la maggior parte dei timeout generali per i nostri selettori come cy.get(...) o cy.contains(...) . Oltre a quel timeout predefinito, vorremmo scalare fino a "breve", "medio", "lungo", "xlong" e "xxlong" all'interno di un oggetto di timeout che possiamo importare ovunque nei nostri file da utilizzare nei nostri comandi Cypress come cy.get(“someElement”, { timeout: timeouts.short }) o cy.task('pluginName', {}, { timeout: timeouts.xlong }) . Dopo aver sostituito i nostri timeout con questi valori dal nostro oggetto importato, abbiamo un posto in cui aggiornare per aumentare o ridurre il tempo necessario per determinati timeout.

Di seguito è mostrato un esempio di come puoi iniziare facilmente con questo.

Avvolgendo

Man mano che le tue suite di test Cypress crescono, potresti riscontrare alcuni degli stessi problemi che abbiamo riscontrato quando abbiamo capito come ridimensionare e mantenere al meglio i tuoi test Cypress. Puoi scegliere di organizzare i tuoi file in base a pagina, funzionalità o qualche altra convenzione di raggruppamento, in modo da sapere sempre dove cercare i file di test esistenti e dove aggiungerne di nuovi man mano che più sviluppatori contribuiscono alla tua base di codice.

Man mano che l'interfaccia utente cambia, puoi utilizzare una sorta di oggetto tipizzato (come i selettori di lettura e scrittura) per mantenere i tuoi selettori di attributo "data-" sugli elementi chiave di ogni pagina su cui vorresti asserire o interagire all'interno dei tuoi unit test e Cypress prove. Se ti ritrovi ad applicare troppi valori arbitrari per cose come i valori di timeout per i tuoi comandi Cypress, potrebbe essere il momento di impostare un oggetto riempito con valori ridimensionati in modo da poter aggiornare quei valori in un unico posto.

Man mano che le cose cambiano nell'interfaccia utente front-end, nell'API back-end e nei test Cypress, dovresti sempre pensare a modi per gestire più facilmente questi test. Meno posti per aggiornare e fare errori e meno ambiguità su dove mettere le cose fanno un'enorme differenza poiché molti sviluppatori aggiungono nuove pagine, funzionalità e (inevitabilmente) test Cypress lungo la strada.

Ti interessano altri post su Cypress? Dai un'occhiata ai seguenti articoli:

  • Cosa considerare quando si scrivono test E2E
  • Panoramica di 1.000 piedi sulla scrittura di test di cipresso
  • TypeScript Tutte le cose nei tuoi test Cypress
  • Gestire i flussi di posta elettronica nei test Cypress
  • Integrazione dei test Cypress con Docker, Buildkite e CICD