Panoramica di 1.000 piedi sulla scrittura di test Cypress #frontend@twiliosendgrid
Pubblicato: 2020-10-03In Twilio SendGrid, abbiamo scritto centinaia di test end-to-end (E2E) Cypress e continuiamo a scrivere di più man mano che nuove funzionalità vengono rilasciate in diverse applicazioni Web e team. Questi test coprono l'intero stack, verificando che i casi d'uso più comuni che un cliente dovrebbe affrontare continuano a funzionare dopo il push di nuove modifiche al codice nelle nostre applicazioni.
Se vuoi prima fare un passo indietro e leggere di più su come pensare ai test E2E in generale, sentiti libero di dare un'occhiata a questo post del blog e torna su questo quando sei pronto. Questo post sul blog non richiede che tu sia un esperto con i test E2E, ma aiuta a entrare nel giusto stato d'animo poiché vedrai perché abbiamo fatto le cose in un certo modo nei nostri test. Se stai cercando un tutorial più dettagliato che ti introduca ai test di Cypress, ti consigliamo di controllare i documenti di Cypress . In questo post del blog, presumiamo che tu abbia già visto o scritto molti test Cypress e siamo curiosi di vedere come altri scrivono test Cypress per le proprie applicazioni.
Dopo aver scritto molti test Cypress, inizierai a notare te stesso usando funzioni, asserzioni e modelli simili di Cypress per ottenere ciò di cui hai bisogno. Ti mostreremo le parti e le strategie più comuni che abbiamo usato o fatto in precedenza con Cypress per scrivere test su ambienti separati come dev o staging. Ci auguriamo che questa panoramica di 1.000 piedi su come scriviamo i test Cypress ti dia idee da confrontare con le tue e ti aiuti a migliorare il modo in cui ti avvicini ai test Cypress.
Contorno:
- Raccolta dell'API di Cypress
- Interagire con gli elementi
- Affermare sugli elementi
- Gestire API e servizi
- Effettuare richieste HTTP con cy.request(…)
- Creazione di plugin riutilizzabili con cy.task()
- Deridere le richieste di rete con cy.server() e cy.route()
- Comandi personalizzati
- Informazioni sugli oggetti di pagina
- Scegliere di non eseguire codice lato client con i controlli window.Cypress
- Gestire gli iframe
- Standardizzazione tra ambienti di test
Raccolta delle API di Cypress
Iniziamo esaminando le parti che abbiamo usato più comunemente con l'API Cypress.
Selezione degli elementi
Esistono molti modi per selezionare gli elementi DOM, ma puoi realizzare la maggior parte di ciò che devi fare tramite questi comandi Cypress e di solito puoi concatenare più azioni e asserzioni dopo questi.
- Ottenere elementi basati su alcuni selettori CSS con
cy.get(“[data-hook='someSelector']”)
ocy.find(“.selector”)
. - Selezionare elementi in base a un testo come
cy.contains(“someText”)
o ottenere un elemento con un determinato selettore che contiene del testo comecy.contains(“.selector”, “someText”)
. - Ottenere un elemento genitore per guardare "dentro", in modo che tutte le tue future query abbiano come ambito i figli del genitore come
cy.get(“.selector”).within(() => { cy.get(“.child”) })
. - Trovare un elenco di elementi ed esaminare "ognuno" per eseguire più query e asserzioni come
cy.get(“tr”).each(($tableRow) => { cy.wrap($tableRow).find('td').eq(1).should(“contain”, “someText” })
. - A volte, gli elementi potrebbero essere fuori dalla visualizzazione della pagina, quindi dovrai prima scorrere l'elemento in vista come
cy.get(“.buttonFarBelow”).scrollIntoView()
. - A volte avrai bisogno di un timeout più lungo rispetto al timeout del comando predefinito, quindi puoi opzionalmente aggiungere un
{ timeout: timeoutInMs }
comecy.get(“.someElement”, { timeout: 10000 })
.
Interagire con gli elementi
Queste sono le interazioni più utilizzate trovate durante i nostri test Cypress. Occasionalmente, dovrai inserire una proprietà { force: true }
in quelle chiamate di funzione per aggirare alcuni controlli con gli elementi. Ciò si verifica spesso quando un elemento è coperto in qualche modo o derivato da una libreria esterna su cui non si ha molto controllo in termini di modalità di rendering degli elementi.
- Abbiamo bisogno di fare clic su molte cose come pulsanti in modali, tabelle e simili, quindi facciamo cose come
cy.get(“.button”).click()
. - I moduli sono ovunque nelle nostre applicazioni web per compilare i dettagli dell'utente e altri campi dati. Digitiamo questi input con
cy.get(“input”).type(“somekeyboardtyping”)
e potrebbe essere necessario cancellare alcuni valori predefiniti degli input cancellandoli prima comecy.get(“input”).clear().type(“somenewinput”)
. Ci sono anche modi interessanti per digitare altri tasti come{enter}
per il tasto Invio quando si eseguecy.get(“input”).type(“text{enter}”)
. - Possiamo interagire con opzioni selezionate come
cy.get(“select”).select(“value”)
e checkbox comecy.get(“.checkbox”).check()
.
Affermare sugli elementi
Queste sono le tipiche asserzioni che puoi usare nei tuoi test Cypress per determinare se le cose sono presenti sulla pagina con il giusto contenuto.
- Per verificare se le cose vengono visualizzate o meno sulla pagina, puoi passare da
cy.get(“.selector”).should(“be.visible”)
ecy.get(“.selector”).should(“not.be.visible”)
. - Per determinare se gli elementi DOM esistono da qualche parte nel markup e se non ti interessa necessariamente se gli elementi sono visibili, puoi usare
cy.get(“.element”).should(“exist”)
ocy.get(“.element”).should(“not.exist”)
).cy.get(“.element”).should(“not.exist”)
. - Per vedere se un elemento contiene o non contiene del testo, puoi scegliere tra
cy.get(“button”).should(“contain”, “someText”)
ecy.get(“button”).should(“not.contain”, “someText”)
. - Per verificare che un input o un pulsante sia disabilitato o abilitato, puoi affermare in questo modo:
cy.get(“button”).should(“be.disabled”)
. - Per affermare se qualcosa è selezionato, puoi provare come,
cy.get(“.checkbox”).should(“be.checked”)
. - Di solito puoi fare affidamento su testi più tangibili e controlli di visibilità, ma a volte devi fare affidamento su controlli di classe come
cy.get(“element”).should(“have.class”, “class-name”)
. Esistono anche altri modi simili per testare gli attributi con.should(“have.attr”, “attribute”)
. - Spesso è utile per te concatenare anche le asserzioni come,
cy.get(“div”).should(“be.visible”).and(“contain”, “text”)
.
Gestire API e servizi
Quando gestisci le tue API e i tuoi servizi relativi alla posta elettronica, puoi utilizzare cy.request(...)
per effettuare richieste HTTP ai tuoi endpoint back-end con intestazioni auth. Un'altra alternativa è che puoi creare plugin cy.task(...)
che possono essere chiamati da qualsiasi file di specifiche per coprire altre funzionalità che possono essere gestite al meglio in un server Node con altre librerie come la connessione a una casella di posta e la ricerca di un corrispondenza della posta elettronica o maggiore controllo sulle risposte e sul polling di determinate chiamate API prima di restituire alcuni valori da utilizzare per i test.
Effettuare richieste HTTP con cy.request(…)
È possibile utilizzare cy.request()
per effettuare richieste HTTP all'API di back-end per configurare o eliminare i dati prima dell'esecuzione dei test case. Di solito si passa l'URL dell'endpoint, il metodo HTTP come "GET" o "POST", le intestazioni e talvolta un corpo della richiesta da inviare all'API back-end. Puoi quindi concatenarlo con un .then((response) => { })
per accedere alla risposta di rete tramite proprietà come "status" e "body". Un esempio di come fare una chiamata cy.request()
è mostrato qui.
A volte, potrebbe non interessarti se cy.request(...)
avrà esito negativo con un codice di stato 4xx o 5xx durante la pulizia prima dell'esecuzione di un test. Uno scenario in cui puoi scegliere di ignorare il codice di stato non riuscito è quando il test effettua una richiesta GET per verificare se un elemento esiste ancora ed è già stato eliminato. L'elemento potrebbe essere già stato ripulito e la richiesta GET avrà esito negativo con un codice di stato 404 non trovato. In questo caso, dovresti impostare un'altra opzione di failOnStatusCode: false
in modo che i tuoi test Cypress non falliscano prima ancora di aver eseguito i passaggi del test.
Creazione di plugin riutilizzabili con cy.task()
Quando vogliamo avere maggiore flessibilità e controllo su una funzione riutilizzabile per parlare con un altro servizio come un provider di posta in arrivo tramite un server Node (ci occuperemo di questo esempio in un post successivo del blog), ci piace fornire le nostre funzionalità extra e le risposte personalizzate alle API ci invitano a concatenare e applicare nei nostri test Cypress. Oppure, ci piace eseguire un altro codice in un server Node: spesso creiamo un plugin cy.task()
per questo. Creiamo le funzioni dei plugin nei file dei moduli e le importiamo in plugins/index.ts
dove definiamo i plugin delle attività con gli argomenti di cui abbiamo bisogno per eseguire le funzioni come mostrato di seguito.
Questi plugin possono essere chiamati con un cy.task(“pluginName”, { ...args })
ovunque nei file delle specifiche e puoi aspettarti che si verifichi la stessa funzionalità. Considerando che, se hai usato cy.request()
, hai meno riusabilità a meno che non hai racchiuso quelle chiamate stesse in oggetti di pagina o file di supporto da importare ovunque.
Un altro avvertimento è che poiché il codice dell'attività del plug-in è pensato per essere eseguito in un server Node, non è possibile chiamare i soliti comandi Cypress all'interno di funzioni come Cypress.env(“apiHost”)
o cy.getCookie('auth_token')
. Passi elementi come la stringa del token di autenticazione o l'host dell'API di back-end all'oggetto argomento della funzione del plug-in oltre alle cose richieste per il corpo della richiesta se ha bisogno di parlare con l'API di back-end.
Deridere le richieste di rete con cy.server() e cy.route()
Per i test Cypress che richiedono dati difficili da riprodurre (come le variazioni di importanti stati dell'interfaccia utente su una pagina o la gestione di chiamate API più lente), una caratteristica di Cypress da considerare è l'eliminazione delle richieste di rete. Funziona bene con le richieste basate su XmlHttpRequest (XHR) se si utilizza vanilla XMLHttpRequest, la libreria axios o jQuery AJAX. Dovresti quindi utilizzare cy.server()
e cy.route()
per ascoltare i percorsi per simulare le risposte per qualsiasi stato desideri. Ecco un esempio:
Un altro caso d'uso consiste nell'usare cy.server()
, cy.route()
e cy.wait()
insieme per ascoltare e attendere che le richieste di rete finiscano prima di eseguire i passaggi successivi. Di solito, dopo aver caricato una pagina o aver eseguito una sorta di azione sulla pagina, un segnale visivo intuitivo segnalerà che qualcosa è completo o pronto per essere affermato e su cui agire. Per i casi in cui non hai un segnale così visibile, puoi attendere esplicitamente che una chiamata API finisca in questo modo.
Un grosso problema è che se stai usando il recupero per le richieste di rete, non sarai in grado di deridere le richieste di rete o aspettare che finiscano allo stesso modo. Avrai bisogno di una soluzione alternativa per sostituire il normale window.fetch
con un polyfill XHR ed eseguire alcuni passaggi di configurazione e pulizia prima che i test vengano eseguiti come registrato in questi problemi . Esiste anche una proprietà experimentalFetchPolyfill
a partire da Cypress 4.9.0 che potrebbe funzionare per te, ma nel complesso, stiamo ancora cercando metodi migliori per gestire lo stubbing della rete attraverso il recupero e l'utilizzo di XHR nelle nostre applicazioni senza che le cose si interrompano. A partire da Cypress 5.1.0, c'è una nuova promettente funzione cy.route2()
(vedi i documenti di Cypress ) per lo stubbing sperimentale di rete sia di XHR che di richieste di recupero, quindi prevediamo di aggiornare la nostra versione di Cypress e sperimentarla per vedere se risolve i nostri problemi.
Comandi personalizzati
Simile a librerie come WebdriverIO, puoi creare comandi personalizzati globali che possono essere riutilizzati e concatenati nei file delle specifiche, ad esempio un comando personalizzato per gestire gli accessi tramite l'API prima dell'esecuzione dei test case. Dopo averli sviluppati in un file come support/commands.ts
, puoi accedere a funzioni come cy.customCommand()
o cy.login()
. La scrittura di un comando personalizzato per l'accesso è simile a questa.
Informazioni sugli oggetti di pagina
Un oggetto pagina è un wrapper attorno a selettori e funzioni per aiutarti a interagire con una pagina. Non è necessario creare oggetti pagina per scrivere i test, ma è bene considerare i modi per incapsulare le modifiche all'interfaccia utente. Vuoi semplificarti la vita in termini di raggruppamento di cose per evitare di aggiornare i selettori e le interazioni in più file anziché in un unico posto.
È possibile definire una classe "Pagina" di base con funzionalità comuni come open()
per le classi di pagine ereditate da condividere ed estendere. Le classi di pagine derivate definiscono le proprie funzioni getter per i selettori e altre funzioni di supporto mentre riutilizzano la funzionalità delle classi base tramite chiamate come super.open()
come mostrato qui.
Scegliere di non eseguire codice lato client con i controlli window.Cypress
Quando abbiamo testato i flussi con file di download automatico come un CSV, i download spesso interrompevano i nostri test Cypress bloccando l'esecuzione del test. Come compromesso, volevamo principalmente verificare se l'utente poteva raggiungere il corretto stato di esito positivo per un download e non scaricare effettivamente il file durante la nostra esecuzione di test aggiungendo un controllo window.Cypress
.
Durante le esecuzioni dei test di Cypress, verrà aggiunta una proprietà window.Cypress
al browser. Nel codice lato client, puoi scegliere di verificare se non è presente alcuna proprietà Cypress sull'oggetto finestra, quindi eseguire il download come di consueto. Ma, se viene eseguito in un test Cypress, non scaricare effettivamente il file. Abbiamo anche approfittato del controllo della proprietà window.Cypress
per i nostri esperimenti A/B in esecuzione nella nostra app web. Non volevamo aggiungere più instabilità e comportamento non deterministico dagli esperimenti A/B che potenzialmente mostrano esperienze diverse ai nostri utenti di test, quindi abbiamo prima verificato che la proprietà non fosse presente prima di eseguire la logica dell'esperimento come evidenziato di seguito.
Gestire gli iframe
Gestire gli iframe può essere difficile con Cypress in quanto non è disponibile il supporto per iframe integrato. Esiste un [problema] in esecuzione ( https://github.com/cypress-io/cypress/issues/136 ) pieno di soluzioni alternative per gestire singoli iframe e iframe nidificati, che possono funzionare o meno a seconda della versione corrente di Cypress o l'iframe con cui intendi interagire. Per il nostro caso d'uso, avevamo bisogno di un modo per gestire gli iframe di fatturazione Zuora nel nostro ambiente di staging per verificare i flussi di aggiornamento dell'API Email e dell'API Marketing Campaigns. I nostri test comportano la compilazione di informazioni di fatturazione di esempio prima di completare l'aggiornamento a una nuova offerta nella nostra app.
Abbiamo creato un comando personalizzato cy.iframe(iframeSelector)
per incapsulare la gestione degli iframe. Il passaggio di un selettore all'iframe controllerà quindi il contenuto del corpo dell'iframe fino a quando non è più vuoto e quindi restituirà il contenuto del corpo per essere concatenato con più comandi Cypress come mostrato di seguito:
Quando si lavora con TypeScript, è possibile digitare il comando personalizzato iframe in questo modo nel file index.d.ts
:
Per completare la parte di fatturazione dei nostri test, abbiamo utilizzato il comando personalizzato iframe per ottenere il contenuto del corpo dell'iframe Zuora, quindi abbiamo selezionato gli elementi all'interno dell'iframe e ne abbiamo modificato direttamente i valori. In precedenza abbiamo avuto problemi con l'utilizzo di cy.find(...).type(...)
e altre alternative non funzionanti, ma per fortuna abbiamo trovato una soluzione alternativa modificando i valori degli input e selezionando direttamente con il comando invoke, ad esempio cy.get(selector).invoke('val', 'some value')
. Avrai anche bisogno ”chromeWebSecurity”: false
nel tuo file di configurazione cypress.json
per consentirti di ignorare eventuali errori di origine incrociata. Di seguito viene fornito un frammento di esempio del suo utilizzo con i selettori di riempimento:
Standardizzazione tra ambienti di test
Dopo aver scritto i test con Cypress utilizzando le asserzioni, le funzioni e gli approcci più comuni evidenziati in precedenza, siamo in grado di eseguire i test e farli superare in un ambiente. Questo è un ottimo primo passo, ma abbiamo più ambienti per distribuire nuovo codice e per testare le nostre modifiche. Ogni ambiente ha il proprio set di database, server e utenti, ma i nostri test Cypress dovrebbero essere scritti una sola volta per funzionare con gli stessi passaggi generali.
Per eseguire i test di Cypress su più ambienti di test come sviluppo, test e staging prima di distribuire le modifiche alla produzione, dobbiamo sfruttare la capacità di Cypress di aggiungere variabili di ambiente e modificare i valori di configurazione per supportare quei casi d'uso.
Per eseguire i test su diversi ambienti frontend :
Dovrai modificare il valore "baseUrl" a cui si accede tramite Cypress.config(“baseUrl”)
in modo che corrisponda a quegli URL come https://staging.app.com o https://testing.app.com . Questo cambia l'URL di base per tutte le tue cy.visit(...)
a cui aggiungere i loro percorsi. Esistono diversi modi per impostarlo, ad esempio impostare CYPRESS_BASE_URL=<frontend_url>
prima di eseguire il comando Cypress o impostare --config baseUrl=<frontend_url>
.
Per eseguire i test su diversi ambienti back-end :
È necessario conoscere il nome host dell'API come https://staging.api.com o https://testing.api.com per impostare una variabile di ambiente come "apiHost" e accedervi tramite chiamate come Cypress.env(“apiHost”)
. Questi verranno utilizzati per le tue cy.request(...)
per effettuare richieste HTTP a determinati percorsi come "<apiHost>/some/endpoint" o passati alle tue chiamate di funzione cy.task(...)
come un altro argomento proprietà per sapere quale backend colpire. Queste chiamate autenticate dovrebbero anche conoscere il token di autenticazione che molto probabilmente stai archiviando in localStorage o un cookie tramite cy.getCookie(“auth_token”)
. Assicurati che questo token di autenticazione venga eventualmente passato come parte dell'intestazione "Autorizzazione" o tramite altri mezzi come parte della tua richiesta. Esistono molti modi per impostare queste variabili di ambiente, ad esempio direttamente nel file cypress.json
o nelle opzioni della riga di comando --env
in cui è possibile fare riferimento ad esse nella documentazione di Cypress .
Per accedere a utenti diversi o utilizzare metadati diversi:
Ora che sai come gestire più URL front-end e host API back-end, come gestisci l'accesso a utenti diversi? Come si utilizzano metadati variabili in base all'ambiente, ad esempio elementi relativi a domini, chiavi API e altre risorse che potrebbero essere univoche negli ambienti di test?
Iniziamo con la creazione di un'altra variabile di ambiente chiamata "testEnv" con possibili valori di "testing" e "staging" in modo da poterla utilizzare come un modo per dire quale ambiente e quali metadati applicare nel test. Usando la variabile d'ambiente "testEnv", puoi avvicinarti a questo in un paio di modi.
Puoi creare file JSON separati "staging.json", "testing.json" e altri file JSON di ambiente nella cartella fixtures
e importarli in modo che possano essere utilizzati in base al valore "testEnv" come cy.fixture(`${testEnv}.json`).then(...)
. Tuttavia, non è possibile digitare bene i file JSON e c'è molto più spazio per errori nella sintassi e nella scrittura di tutte le proprietà richieste per test. I file JSON sono anche più lontani dal codice del test, quindi dovresti gestire almeno due file durante la modifica dei test. Problemi di manutenzione simili si verificherebbero se tutti i dati dei test ambientali fossero impostati nelle variabili di ambiente direttamente nel tuo cypress.json
e ce ne sarebbero troppi da gestire in una pletora di test.
Un'opzione alternativa consiste nel creare un oggetto dispositivo di test all'interno del file delle specifiche con proprietà basate su test o staging per caricare l'utente e i metadati del test per un determinato ambiente. Poiché si tratta di oggetti, puoi anche definire un tipo TypeScript generico migliore attorno agli oggetti dispositivo di prova per tutti i file delle specifiche da riutilizzare e definire i tipi di metadati. Dovresti chiamare Cypress.env(“testEnv”)
per vedere in quale ambiente di test stai eseguendo e utilizzare quel valore per estrarre il dispositivo di test dell'ambiente corrispondente dall'oggetto dispositivo di test generale e utilizzare quei valori nel test. L'idea generale dell'oggetto test fixtures è riassunta nel frammento di codice sottostante.
L'applicazione insieme del valore di configurazione Cypress "baseUrl", della variabile di ambiente back-end "apiHost" e della variabile di ambiente "testEnv" ci consente di avere test Cypress che funzionano su più ambienti senza aggiungere più condizioni o flussi logici separati, come illustrato di seguito.
Facciamo un passo indietro per vedere come puoi persino creare i tuoi comandi Cypress da eseguire tramite npm. Concetti simili possono essere applicati a filato, Makefile e altri script che potresti utilizzare per la tua applicazione. Potresti voler definire variazioni dei comandi "apri" ed "esegui" per allinearti con Cypress "apri" la GUI ed "esegui" in modalità headless contro vari ambienti front-end e back-end nel tuo package.json
. Puoi anche impostare più file JSON per la configurazione di ogni ambiente, ma per semplicità vedrai i comandi con le opzioni e i valori in linea.
Noterai negli script package.json
che il tuo frontend "baseUrl" varia da "http://localhost:9001" per quando avvii l'app localmente all'URL dell'applicazione distribuita come " https://staging.app. com ”. È possibile impostare le variabili di back-end "apiHost" e "testEnv" per facilitare l'invio di richieste a un endpoint di back-end e il caricamento di un oggetto dispositivo di test specifico. Puoi anche creare comandi "cicd" speciali per quando devi eseguire i test in un contenitore Docker con la chiave di registrazione.
Alcuni asporto
Quando si tratta di selezionare elementi, interagire con elementi e affermare elementi sulla pagina, si può arrivare molto lontano scrivendo molti test Cypress con un piccolo elenco di comandi Cypress come cy.get()
, cy.contains()
, .click( .click()
, .type()
, .should('be.visible')
.
Esistono anche modi per effettuare richieste HTTP a un'API di back-end utilizzando cy.request()
, eseguire codice arbitrario in un server Node con cy.task()
e bloccare le richieste di rete utilizzando cy.server()
e cy.route()
. Puoi persino creare il tuo comando personalizzato come cy.login()
per aiutarti ad accedere a un utente tramite l'API. Tutte queste cose aiutano a ripristinare un utente al punto di partenza corretto prima dell'esecuzione dei test. Avvolgi questi selettori e funzioni insieme in un file e hai creato oggetti di pagina riutilizzabili da utilizzare nelle tue specifiche.
Per aiutarti a scrivere test che superano più di un ambiente, sfrutta le variabili di ambiente e gli oggetti che contengono metadati specifici dell'ambiente.
Ciò ti aiuterà a eseguire diversi insiemi di utenti con risorse di dati separate nelle tue specifiche Cypress. Separare i comandi Cypress npm come npm run cypress:open:staging
nel tuo package.json
caricherà i valori delle variabili di ambiente corretti ed eseguirà i test per l'ambiente in cui hai scelto di eseguire.
Questo conclude la nostra panoramica di mille piedi sulla scrittura dei test Cypress. Ci auguriamo che questo ti abbia fornito esempi pratici e modelli da applicare e migliorare nei tuoi test Cypress.
Vuoi saperne di più sui test Cypress? Dai un'occhiata alle seguenti risorse:
- Cosa considerare quando si scrivono test E2E
- TypeScript Tutte le cose nei tuoi test Cypress
- Gestire i flussi di posta elettronica nei test Cypress
- Idee per configurare, organizzare e consolidare i test Cypress
- Integrazione dei test Cypress con Docker, Buildkite e CICD