Un viaggio di test E2E Parte 1: da STUI a WebdriverIO
Pubblicato: 2019-11-21Nota: questo è un post di #frontend@twiliosendgrid. Per altri post di ingegneria, vai al rotolo del blog tecnico.
Poiché l'architettura frontend di SendGrid ha iniziato a maturare nelle nostre applicazioni Web, abbiamo voluto aggiungere un altro livello di test oltre al nostro solito livello di test di unità e integrazione. Abbiamo cercato di creare nuove pagine e funzionalità con copertura di test E2E (end-to-end) con strumenti di automazione del browser.
Volevamo automatizzare i test dal punto di vista del cliente ed evitare test di regressione manuali, ove possibile, per eventuali grandi cambiamenti che potrebbero verificarsi in qualsiasi parte dello stack. Avevamo, e abbiamo tuttora, il seguente obiettivo: fornire un modo per scrivere test di automazione E2E coerenti, di cui è possibile eseguire il debug, manutenibili e di valore per le nostre applicazioni front-end e integrarli con CICD (integrazione continua e distribuzione continua).
Abbiamo sperimentato molteplici approcci tecnici fino a quando non abbiamo finalizzato la nostra soluzione ideale per i test E2E. Ad alto livello, questo riassume il nostro viaggio:
- Costruire la nostra soluzione personalizzata Ruby Selenium interna, SiteTestUI alias STUI
- Transizione da STUI a un WebdriverIO basato su nodi
- Non essere soddisfatto di nessuna delle due impostazioni e finalmente migrare su Cypress
Questo post del blog è una delle due parti che documentano ed evidenziano le nostre esperienze, le lezioni apprese e i compromessi con ciascuno degli approcci utilizzati lungo il percorso per guidare, si spera, te e altri sviluppatori su come collegare i test E2E con modelli e strategie di test utili.
La prima parte comprende le nostre prime lotte con STUI, il modo in cui siamo migrati a WebdriverIO e tuttavia abbiamo subito molte cadute simili a STUI. Esamineremo come abbiamo scritto i test con WebdriverIO, Dockerizzato i test per l'esecuzione in un container e, infine, integrato i test con Buildkite, il nostro provider CICD.
Se desideri saltare al punto in cui siamo con i test E2E oggi, vai alla parte due mentre passa attraverso la nostra migrazione finale da STUI e WebdriverIO a Cypress e come l'abbiamo configurata tra diversi team.
TLDR: Abbiamo sperimentato dolori e difficoltà simili con entrambe le soluzioni di wrapper Selenium, STUI e WebdriverIO, che alla fine abbiamo iniziato a cercare alternative in Cypress. Abbiamo imparato una serie di lezioni approfondite per affrontare la scrittura di test E2E e l'integrazione con Docker e Buildkite.
Sommario:
Prima incursione nei test E2E: siteTESUI alias STUI
Passaggio da STUI a WebdriverIO
Passaggio 1: decisione sulle dipendenze per WebdriverIO
Passaggio 2: configurazioni e script dell'ambiente
Passaggio 3: implementazione dei test ENE a livello locale
Passaggio 4: Dockerizzazione di tutti i test
Passaggio 5: integrazione con CICD
Compromessi con WebdriverIo
Passando al Cipresso
Prima incursione nei test E2E: SiteTestUI alias STUI
Quando cercavano inizialmente uno strumento di automazione del browser, i nostri SDET (ingegneri di sviluppo software in prova) si sono tuffati nel creare la nostra soluzione interna personalizzata costruita con Ruby e Selenium, in particolare Rspec e un framework Selenium personalizzato chiamato Gridium. Abbiamo apprezzato il suo supporto cross-browser, la capacità di configurare le nostre integrazioni personalizzate con TestRail per i nostri test case per ingegneri QA (Quality Assurance) e l'idea di creare il repository ideale per tutti i team frontend per scrivere test E2E in un'unica posizione ed essere correre in base a un programma.
Come sviluppatore frontend desideroso di scrivere per la prima volta alcuni test E2E con gli strumenti che gli SDET hanno creato per noi, abbiamo iniziato a implementare i test per le pagine che abbiamo già rilasciato e abbiamo riflettuto su come impostare correttamente utenti e dati di seed per concentrarci su parti di la funzione che volevamo testare. Abbiamo imparato alcune grandi cose lungo il percorso come formare oggetti di pagina per organizzare le funzionalità di supporto e selettori di elementi con cui desideriamo interagire per pagina e abbiamo iniziato a formare specifiche che seguivano questa struttura:
Abbiamo gradualmente creato suite di test sostanziali tra diversi team nello stesso repository seguendo schemi simili, ma presto abbiamo sperimentato molte frustrazioni che avrebbero rallentato enormemente i nostri progressi per i nuovi sviluppatori e i contributori coerenti a STUI come:
- L'avvio e l'esecuzione hanno richiesto molto tempo e sforzi per installare tutti i driver del browser, le dipendenze di Ruby Gem e le versioni corrette prima ancora di eseguire le suite di test. A volte dovevamo capire perché i test sarebbero stati eseguiti sulla macchina di una persona rispetto alla macchina di un'altra persona e in che modo le loro impostazioni differivano.
- Le suite di test sono proliferate e sono state eseguite per ore fino al completamento. Poiché tutti i team hanno contribuito allo stesso repository, eseguire tutti i test in serie significava aspettare diverse ore per l'esecuzione della suite di test generale e più team che spingevano nuovo codice potenzialmente portavano a un altro test interrotto da qualche altra parte.
- Siamo diventati frustrati dai selettori CSS traballanti e dai complicati selettori XPath . Questa immagine qui sotto spiega abbastanza come l'uso di XPath può rendere le cose più complicate e queste erano alcune delle più semplici.
- Il debug dei test è stato doloroso . Abbiamo avuto problemi a eseguire il debug di vaghi output di errore e di solito non avevamo idea di dove e come le cose non funzionassero. Potremmo solo eseguire ripetutamente i test e osservare il browser per dedurre dove è possibile che abbia fallito e quale codice ne fosse responsabile. Quando un test ha fallito in un ambiente Docker in CICD senza molto da guardare oltre agli output della console, abbiamo faticato a riprodurre localmente e a risolvere il problema.
- Abbiamo riscontrato bug di selenio e lentezza . I test sono stati eseguiti lentamente a causa di tutte le richieste inviate dal server al browser e, a volte, i nostri test si bloccavano del tutto quando si tentava di selezionare molti elementi sulla pagina o per motivi sconosciuti durante l'esecuzione dei test.
- È stato speso più tempo per correggere e saltare i test e le esecuzioni di test di build pianificate interrotte hanno iniziato a essere ignorate. I test non hanno fornito valore nel significare effettivamente veri errori nel sistema.
- I nostri team di frontend si sono sentiti disconnessi dai test E2E poiché esisteva in un repository separato da quelli delle rispettive applicazioni web. Spesso dovevamo aprire entrambi i repository contemporaneamente e continuare a guardare avanti e indietro tra le basi di codice oltre alle schede del browser durante l'esecuzione dei test.
- Ai team di frontend non piaceva il passaggio dal contesto alla scrittura di codice in JavaScript o TypeScript quotidianamente a Ruby e il dover reimparare a scrivere i test ogni volta che si contribuisce a STUI.
- Dal momento che è stata la prima volta per molti di noi quando abbiamo contribuito al test, siamo caduti in molti antipattern come la creazione di uno stato tramite l'interfaccia utente per l'accesso, lo smontaggio o la configurazione insufficiente tramite l'API e la documentazione insufficiente da seguire per quello che fa un grande test.
Nonostante i nostri progressi nella scrittura di un numero considerevole di test E2E per molti team diversi in un repository e l'apprendimento di alcuni modelli utili da portare con noi, abbiamo riscontrato mal di testa con l'esperienza complessiva degli sviluppatori, molteplici punti di errore e mancanza di test validi e stabili per verificare il nostro intero stack.
Abbiamo apprezzato un modo per consentire ad altri sviluppatori frontend e QA di creare le proprie suite di test E2E stabili con JavaScript che risiede con il codice dell'applicazione per promuovere il riutilizzo, la vicinanza e la proprietà dei test. Questo ci ha portato a sondare WebdriverIO, un framework Selenium basato su JavaScript per i test di automazione dei browser, come nostro sostituto iniziale di STUI, la soluzione interna personalizzata di Ruby Selenium.
In seguito ne sperimenteremo le cadute e alla fine passeremo a Cypress (avanti rapidamente alla Parte 2 qui se le cose di WebdriverIO non ti piacciono), ma abbiamo acquisito un'esperienza preziosa con la creazione di un'infrastruttura standardizzata nel repository di ogni team, integrando i test E2E in CICD per il nostro frontend team e adottando modelli tecnici su cui vale la pena documentare nel nostro viaggio e per consentire ad altri di conoscere chi potrebbe essere in procinto di entrare in WebdriverIO o in qualsiasi altra soluzione di test E2E.
Passaggio da STUI a WebdriverIO
Quando ci siamo imbarcati su WebdriverIO per alleviare le frustrazioni che abbiamo riscontrato, abbiamo sperimentato che ogni team di frontend converte i test di automazione esistenti scritti con l'approccio Ruby Selenium ai test di WebdriverIO in JavaScript o TypeScript e confrontando stabilità, velocità, esperienza degli sviluppatori e manutenzione generale di i test.
Al fine di ottenere la nostra configurazione ideale per avere test E2E che risiedono nei repository di applicazioni dei team frontend e che vengono eseguiti sia in CICD che in pipeline programmate, abbiamo riassunto i seguenti passaggi che generalmente si applicano a qualsiasi team che desideri integrare un framework di test E2E con obiettivi simili :
- Installazione e scelta delle dipendenze da collegare al framework di test
- Stabilire configurazioni di ambiente e comandi di script
- Implementazione di test E2E che superano localmente ambienti diversi
- Dockerizzazione dei test
- Integrazione dei test Dockerizzati con il provider CICD
Passaggio 1: decisione sulle dipendenze per WebdriverIO
WebdriverIO offre agli sviluppatori la flessibilità di scegliere tra molti framework, reporter e servizi con cui avviare il test runner. Ciò ha richiesto molti armeggi e ricerche affinché i team si stabilissero su determinate librerie per iniziare.
Poiché WebdriverIO non è prescrittivo su cosa utilizzare, ha aperto la porta ai team di frontend per avere librerie e configurazioni variabili, sebbene i test di base complessivi sarebbero coerenti nell'utilizzo dell'API WebdriverIO.
Abbiamo scelto di consentire a ciascuno dei team di frontend di personalizzare in base alle proprie preferenze e in genere abbiamo utilizzato Mocha come framework di test, Mochawesome come reporter, il servizio Selenium Standalone e il supporto Typescript. Abbiamo scelto Mocha e Mochawesome per la familiarità e l'esperienza precedente con Mocha dei nostri team, ma anche altri team hanno deciso di utilizzare altre alternative.
Passaggio 2: configurazioni e script dell'ambiente
Dopo aver deciso l'infrastruttura WebdriverIO, avevamo bisogno di un modo per eseguire i nostri test WebdriverIO con impostazioni diverse per ogni ambiente. Ecco un elenco che illustra la maggior parte dei casi d'uso di come volevamo eseguire questi test e perché desideravamo supportarli:
- Contro un server di sviluppo Webpack in esecuzione su localhost (ad esempio http://localhost:8000) e quel server di sviluppo verrebbe indirizzato a una determinata API dell'ambiente come test o staging (ad esempio https://testing.api.com o https:// staging.api.com).
Come mai? A volte abbiamo bisogno di apportare modifiche alla nostra app Web locale, ad esempio aggiungendo selettori più specifici per i nostri test per interagire con gli elementi in un modo più robusto o stavamo sviluppando una nuova funzionalità e avevamo bisogno di regolare e convalidare i test di automazione esistenti passerebbe localmente contro le nostre nuove modifiche al codice. Ogni volta che il codice dell'applicazione è cambiato e non abbiamo ancora eseguito il push-up nell'ambiente distribuito, abbiamo utilizzato questo comando per eseguire i test sulla nostra app Web locale. - Contro un'app distribuita per un determinato ambiente (ad esempio https://testing.app.com o https://staging.app.com) come test o staging
Come mai? Altre volte il codice dell'applicazione non cambia, ma potremmo dover modificare il nostro codice di test per correggere alcune imperfezioni o ci sentiamo abbastanza sicuri da aggiungere o eliminare del tutto i test senza apportare modifiche al frontend. Abbiamo ampiamente utilizzato questo comando per aggiornare o eseguire il debug dei test in locale rispetto all'app distribuita per simulare più da vicino il modo in cui i nostri test vengono eseguiti nelle pipeline CICD. - In esecuzione in un contenitore Docker rispetto a un'app distribuita per un determinato ambiente come test o staging
Come mai? Questo è pensato per le pipeline CICD in modo che possiamo attivare i test E2E da eseguire in un container Docker rispetto, ad esempio, all'app distribuita di staging e assicurarci che passino prima di distribuire il codice alla produzione o in esecuzioni di test pianificate in una pipeline dedicata. Durante l'impostazione iniziale di questi comandi, abbiamo eseguito molti tentativi ed errori per avviare i contenitori Docker con valori di variabili di ambiente diversi e testare per vedere i test corretti eseguiti con successo prima di collegarlo al nostro provider CICD, Buildkite.
Per ottenere ciò, impostiamo un file di configurazione di base generale con proprietà condivise e molti file specifici dell'ambiente in modo tale che ogni file di configurazione dell'ambiente si unisca al file di base e sovrascriva o aggiunga le proprietà richieste per l'esecuzione. Avremmo potuto avere un file per ogni ambiente senza la necessità di un file di base, ma ciò avrebbe portato a molte duplicazioni nelle impostazioni comuni. Abbiamo scelto di utilizzare una libreria come deepmerge
per gestirla per noi, ma è importante notare che l'unione non è sempre perfetta con oggetti o array nidificati. Ricontrolla sempre le configurazioni di output risultanti in quanto potrebbe portare a un comportamento indefinito quando sono presenti proprietà duplicate che non sono state unite correttamente.
Abbiamo formato un file di configurazione di base comune , wdio.conf.js
, come questo:
Per adattarsi al nostro primo caso d'uso principale dell'esecuzione di test E2E su un server di sviluppo webpack locale puntato a un'API di ambiente, abbiamo generato il file di configurazione localhost, wdio.localhost.conf.js
, nel modo seguente:
Si noti che abbiamo unito il file di base e aggiunto le proprietà specifiche di localhost al file per renderlo più compatto e di facile manutenzione. Utilizziamo anche il servizio Selenium Standalone per avviare diversi tipi di browser, ovvero funzionalità.
Per il secondo caso d'uso dell'esecuzione di test E2E su un'app Web distribuita, abbiamo impostato i file di configurazione dell'app di test e staging , `wdio.testing.conf.js` e wdio.staging.conf.js
, simili a questo:
Qui abbiamo aggiunto alcune variabili di ambiente extra ai file di configurazione come le credenziali di accesso agli utenti dedicati allo staging e aggiornato `baseUrl` in modo che punti all'URL dell'app di staging distribuita.
Per il terzo caso d'uso dell'esecuzione di test E2E in un contenitore Docker rispetto a un'app Web distribuita nell'ambito del nostro provider CICD, abbiamo impostato i file di configurazione CICD , wdio.cicd.testing.conf.js
e wdio.cicd.staging.conf.js
, in questo modo:
Nota come non utilizziamo più il servizio Selenium Standalone poiché in seguito installeremo Selenium Chrome, Selenium Hub e il codice dell'applicazione in servizi separati in un file Docker Compose. Questa configurazione mostra anche le stesse variabili di ambiente della configurazione di staging come le credenziali di accesso e `baseUrl` poiché prevediamo di eseguire i nostri test sull'app di staging distribuita e l'unica differenza è che questi test sono pensati per essere eseguiti all'interno di un contenitore Docker .
Dopo aver stabilito questi file di configurazione dell'ambiente, abbiamo delineato i comandi di script package.json
che sarebbero serviti come base per i nostri test. Per questo esempio, abbiamo preceduto i comandi con "uitest" per indicare i test dell'interfaccia utente con WebdriverIO e perché abbiamo anche terminato i file di test con *.uitest.js
. Di seguito sono riportati alcuni comandi di esempio per l'ambiente di staging:
Passaggio 3: implementazione dei test E2E a livello locale
Con tutti i comandi di test a portata di mano, abbiamo individuato i test nel nostro repository STUI per convertirli in test WebdriverIO. Ci siamo concentrati sui test di pagina di piccole e medie dimensioni e abbiamo iniziato ad applicare il modello di oggetto della pagina per incapsulare tutta l'interfaccia utente per ogni pagina in modo organizzato.
Potremmo avere file strutturati con una serie di funzioni di supporto o valori letterali di oggetti o qualsiasi altra strategia, ma la chiave era disporre di un modo coerente per fornire rapidamente test gestibili e attenersi ad esso. Se il flusso dell'interfaccia utente o gli elementi DOM sono stati modificati per una pagina specifica, è stato necessario solo refactoring dell'oggetto pagina ad esso correlato e possibilmente il codice di test per ottenere nuovamente il superamento dei test.
Abbiamo implementato il pattern dell'oggetto della pagina disponendo di un oggetto della pagina di base con funzionalità condivise da cui si estenderebbero tutti gli altri oggetti della pagina. Avevamo funzioni come open per fornire un'API coerente su tutti gli oggetti della nostra pagina per "aprire" o visitare l'URL di una pagina nel browser. Assomigliava a qualcosa del genere:
L'implementazione degli specifici oggetti della pagina ha seguito lo stesso schema di estensione dalla classe Page
di base e l'aggiunta dei selettori a determinati elementi con cui desideravamo interagire o su cui si desiderava affermare e funzioni di supporto per eseguire azioni sulla pagina.
Nota come abbiamo usato la classe base open tramite super.open(...)
con il percorso specifico della pagina in modo da poter visitare la pagina con questa chiamata, SomePage.open()
. Abbiamo anche esportato la classe già inizializzata in modo da poter fare riferimento a elementi come SomePage.submitButton
o SomePage.tableRows
e interagire o asserire su quegli elementi con i comandi WebdriverIO. Se l'oggetto pagina doveva essere condiviso e inizializzato con le proprie proprietà membro in un costruttore, potremmo anche esportare la classe direttamente e creare un'istanza dell'oggetto pagina nei file di test con new SomePage(...constructorArgs)
.
Dopo aver disposto gli oggetti della pagina con i selettori e alcune funzionalità di supporto, abbiamo quindi scritto i test E2E e comunemente modellato questa formula di test:
- Impostare o smontare tramite l'API ciò che è necessario per ripristinare le condizioni di test al punto di partenza previsto prima di eseguire i test effettivi.
- Accedi a un utente dedicato per il test, quindi ogni volta che visitavamo le pagine direttamente rimarremmo connessi e non dovemmo passare attraverso l'interfaccia utente. Abbiamo creato una semplice funzione di supporto per l'
login
che accetta un nome utente e una password che effettua la stessa chiamata API che utilizziamo per la nostra pagina di accesso e che alla fine restituisce il nostro token di autenticazione necessario per rimanere connesso e per passare le intestazioni delle richieste API protette. Altre aziende potrebbero disporre di endpoint interni o strumenti ancora più personalizzati per creare rapidamente utenti nuovi di zecca con dati di seeding e configurazioni, ma noi, sfortunatamente, non ne avevamo uno abbastanza sviluppato. Lo faremmo alla vecchia maniera e creeremo utenti di test dedicati nei nostri ambienti con diverse configurazioni attraverso l'interfaccia utente e spesso interrompiamo i test per pagine con utenti distinti per evitare conflitti di risorse e rimanere isolati quando i test vengono eseguiti in parallelo. Dovevamo assicurarci che gli utenti di test dedicati non fossero toccati da altri, altrimenti i test si sarebbero interrotti quando qualcuno inconsapevolmente ha armeggiato con uno di loro. - Automatizza i passaggi come se un utente finale interagisse con la funzione/pagina. In genere, visitiamo la pagina che contiene il nostro flusso di funzionalità e iniziamo a seguire i passaggi di alto livello che un utente finale farebbe, come compilare input, fare clic sui pulsanti, attendere che appaiano modali o banner e osservare le tabelle per gli output modificati come un risultato dell'azione. Usando i nostri pratici oggetti e selettori della pagina, abbiamo implementato rapidamente ogni passaggio e, come controlli di integrità lungo il percorso, avremmo affermato su ciò che l'utente dovrebbe o non dovrebbe vedere sulla pagina durante il flusso delle funzionalità per essere certi che le cose si comportino come previsto prima e dopo ogni passaggio. Abbiamo anche deciso di scegliere test di percorso felice di alto valore e talvolta stati di errore comuni facilmente riproducibili, rinviando il resto dei test di livello inferiore ai test unitari e di integrazione.
Ecco un esempio approssimativo del layout generale dei nostri test E2E (questa strategia si applicava anche ad altri framework di test che abbiamo provato):
In una nota a margine, abbiamo deciso di non coprire tutti i suggerimenti e i trucchi per le migliori pratiche WebdriverIO ed E2E in questa serie di post sul blog, ma parleremo di questi argomenti in un futuro post sul blog, quindi resta sintonizzato!
Passaggio 4: Dockerizzazione di tutti i test
Durante l'esecuzione di ogni passaggio della pipeline Buildkite su una nuova macchina AWS nel cloud, non potevamo semplicemente chiamare "npm run uitests: staging" perché quelle macchine non hanno Node, browser, il nostro codice dell'applicazione o qualsiasi altra dipendenza per eseguire effettivamente i test .
Per risolvere questo problema, abbiamo raggruppato tutte le dipendenze come Node, Selenium, Chrome e il codice dell'applicazione in un contenitore Docker affinché i test WebdriverIO possano essere eseguiti correttamente. Abbiamo sfruttato Docker e Docker Compose per assemblare tutti i servizi necessari per iniziare a funzionare, che si sono tradotti in Dockerfiles
e file docker-compose.yml
e molta sperimentazione con la rotazione dei contenitori Docker in locale per far funzionare le cose.
Per fornire più contesto, non eravamo esperti in Docker, quindi ci è voluto molto tempo per capire come mettere insieme le cose. Esistono diversi modi per Dockerizzare i test di WebdriverIO e abbiamo trovato difficile orchestrare insieme molti servizi diversi e passare al setaccio le diverse immagini Docker, Compose versioni e tutorial finché le cose non hanno funzionato.
Dimostreremo file per lo più arricchiti che corrispondono a una delle configurazioni dei nostri team e speriamo che questo fornisca approfondimenti per te o per chiunque affronti il problema generale dei test basati su Dockerizing Selenium.
Ad alto livello, i nostri test richiedevano quanto segue:
- Selenium per eseguire comandi e comunicare con un browser. Abbiamo utilizzato Selenium Hub per creare più istanze a piacimento e abbiamo scaricato l'immagine, "selenium/hub", per il servizio
selenium-hub
nel file docker-compose. - Un browser contro cui correre. Abbiamo richiamato le istanze di Selenium Chrome e installato l'immagine, "selenium/node-chrome-debug", per il servizio selenium-
selenium-chrome
neldocker-compose.yml file
. - Codice dell'applicazione per eseguire i nostri file di test con qualsiasi altro modulo Node installato. Abbiamo creato un nuovo
Dockerfile
per fornire un ambiente con Node per installare pacchetti npm ed eseguire scriptpackage.json
, copiare il codice di test e assegnare un servizio dedicato all'esecuzione dei file di test denominatouitests
nel filedocker-compose.yml
.
Per aprire un servizio con tutta la nostra applicazione e il codice di test necessari per eseguire i test WebdriverIO, abbiamo creato un Dockerfile
chiamato Dockerfile.uitests
e installato tutti i node_modules
e copiato il codice nella directory di lavoro dell'immagine in un ambiente Node. Questo verrebbe utilizzato dal nostro servizio Docker Compose di uitests
e abbiamo ottenuto la configurazione di Dockerfile
nel modo seguente:
Per visualizzare insieme Selenium Hub, browser Chrome e codice di test dell'applicazione per l'esecuzione dei test WebdriverIO, abbiamo delineato i servizi selenium selenium-hub
, selenium selenium-chrom
e e uitest
nel file docker-compose.uitests.yml
:
Abbiamo collegato Selenium Hub e le immagini di Chrome tramite variabili di ambiente, depends_on
ed esponendo le porte ai servizi. La nostra immagine del codice dell'applicazione di prova verrebbe infine caricata ed estratta da un registro Docker privato che gestiamo.
Costruiremmo l'immagine Docker per il codice di test durante CICD con alcune variabili di ambiente come VERSION
e PIPELINE_SUFFIX
per fare riferimento alle immagini con un tag e un nome più specifico. Avremmo quindi avviato i servizi Selenium ed eseguito comandi tramite il servizio uitests
per eseguire i test WebdriverIO.
Durante la creazione dei nostri file Docker Compose, abbiamo sfruttato i comandi utili come docker-compose up
e docker-compose down
con il Mac Docker installato sulle nostre macchine per testare localmente le nostre immagini avevano le configurazioni corrette e funzionavano senza intoppi prima dell'integrazione con Buildkite. Abbiamo documentato tutti i comandi necessari per costruire le immagini contrassegnate, inviarle al registro, tirarle verso il basso ed eseguire i test in base ai valori delle variabili di ambiente.
Passaggio 5: integrazione con CICD
Dopo aver stabilito i comandi Docker funzionanti e dopo che i nostri test sono stati eseguiti con successo all'interno di un container Docker in ambienti diversi, abbiamo iniziato a integrarci con Buildkite, il nostro provider CICD.
Buildkite ha fornito modi per eseguire passaggi in un file .yml
sulle nostre macchine AWS con script Bash e variabili di ambiente impostate tramite il codice o l'interfaccia utente delle impostazioni di Buildkite per la pipeline del nostro repository.
Buildkite ci ha anche consentito di attivare questa pipeline di test dalla nostra pipeline di distribuzione principale con variabili di ambiente esportate e avremmo riutilizzato questi passaggi di test per altre pipeline di test isolate che sarebbero state eseguite in base a una pianificazione per il monitoraggio e l'analisi dei nostri QA.
Ad alto livello, le nostre pipeline di test Buildkite per WebdriverIO e successive Cypress hanno condiviso i seguenti passaggi simili:
- Configura le immagini Docker . Crea, tagga e invia le immagini Docker richieste per i test al registro in modo da poterle estrarre in un passaggio successivo.
- Eseguire i test in base alle configurazioni delle variabili di ambiente . Apri le immagini Docker contrassegnate per la build specifica ed esegui i comandi appropriati su un ambiente distribuito per eseguire suite di test selezionate dalle variabili di ambiente impostate.
Ecco un esempio ravvicinato di un file pipeline.uitests.yml
che mostra la configurazione delle immagini Docker nel passaggio "Crea immagine Docker UITests" e l'esecuzione dei test nel passaggio "Esegui test Webdriver su Chrome":
Una cosa da notare è il primo passaggio, "Crea immagine Docker UITests" e come imposta le immagini Docker per il test. Ha utilizzato il comando Docker Compose build
per creare il servizio uitests
con tutto il codice di test dell'applicazione e lo ha contrassegnato con la variabile di ambiente latest
e ${VERSION}
in modo da poter eventualmente estrarre la stessa immagine con il tag appropriato per questa build in futuro fare un passo.
Ogni passaggio può essere eseguito su una macchina diversa nel cloud AWS da qualche parte, quindi i tag identificano in modo univoco l'immagine per la specifica esecuzione di Buildkite. Dopo aver taggato l'immagine, abbiamo trasferito l'immagine più recente e contrassegnata con la versione nel nostro registro Docker privato per essere riutilizzata.
Nel passaggio "Esegui test di Webdriver su Chrome", estraiamo l'immagine che abbiamo creato, contrassegnato e inserito nel primo passaggio e avviamo Selenium Hub, Chrome e i servizi di test. Sulla base di variabili di ambiente come $UITESTENV
e $UITESTSUITE
, sceglieremo il tipo di comando da eseguire come npm run uitest:
e le suite di test da eseguire per questa build Buildkite specifica come --suite $UITESTSUITE
.
Queste variabili di ambiente verrebbero impostate tramite le impostazioni della pipeline Buildkite o verrebbero attivate dinamicamente da uno script Bash che analizzerebbe un campo di selezione Buildkite per determinare quali suite di test eseguire e in quale ambiente.
Ecco un esempio di test WebdriverIO attivati in una pipeline di test dedicata, che ha anche riutilizzato lo stesso file pipeline.uitests.yml
ma con variabili di ambiente impostate in cui è stata attivata la pipeline. Questa build non è riuscita e presentava schermate di errore da esaminare nella scheda Artifacts
e nell'output della console nella scheda Logs
. Ricorda gli artifact_paths
in pipeline.uitests.yml
(https://gist.github.com/alfredlucero/71032a82f3a72cb2128361c08edbcff2#file-pipeline-uitests-yml-L38), le impostazioni degli screenshot per `mochawesome` in `wdio.conf.js ` (https://gist.github.com/alfredlucero/4ee280be0e0674048974520b79dc993a#file-wdio-conf-js-L39), e montaggio dei volumi nel servizio `uitests` nel `docker-compose.uitests.yml` (https://gist.github.com/alfredlucero/d2df4533a4a49d5b2f2c4a0eb5590ff8#file-docker-compose-yml-L32)?
Siamo stati in grado di collegare gli screenshot in modo che fossero accessibili tramite l'interfaccia utente di Buildkite per consentirci di scaricarli direttamente e visualizzarli immediatamente per aiutare con i test di debug come mostrato di seguito.
Di seguito viene visualizzato un altro esempio di test WebdriverIO eseguiti in una pipeline separata su una pianificazione per una pagina specifica utilizzando il file pipeline.uitests.yml
, ad eccezione delle variabili di ambiente già configurate nelle impostazioni della pipeline Buildkite.
È importante notare che ogni provider CICD ha funzionalità e modi diversi per integrare i passaggi in una sorta di processo di distribuzione durante l'unione in un nuovo codice, sia che si tratti di file .yml
con sintassi specifica, impostazioni della GUI, script Bash o qualsiasi altro mezzo.
Quando siamo passati da Jenkins a Buildkite, abbiamo migliorato drasticamente la possibilità per i team di definire le proprie pipeline all'interno delle rispettive basi di codice, parallelizzando i passaggi tra le macchine di scalabilità su richiesta e utilizzando comandi di più facile lettura.
Indipendentemente dal provider CICD che potresti utilizzare, le strategie di integrazione dei test saranno simili nella configurazione delle immagini Docker e nell'esecuzione dei test in base alle variabili di ambiente per la portabilità e la flessibilità.
Compromessi con WebdriverIO
Dopo aver convertito un numero considerevole di test della soluzione Ruby Selenium personalizzati in test WebdriverIO e l'integrazione con Docker e Buildkite, abbiamo migliorato in alcune aree, ma abbiamo comunque riscontrato difficoltà simili al vecchio sistema che alla fine ci ha portato alla nostra prossima e ultima tappa con Cypress per la nostra soluzione di test E2E.
Ecco un elenco di alcuni dei vantaggi che abbiamo riscontrato dalle nostre esperienze con WebdriverIO rispetto alla soluzione personalizzata Ruby Selenium:
- I test sono stati scritti esclusivamente in JavaScript o TypeScript anziché in Ruby . Ciò significava meno cambio di contesto tra le lingue e meno tempo speso per riapprendere Ruby ogni volta che scrivevamo test E2E.
- Abbiamo collocato i test con il codice dell'applicazione anziché in un repository condiviso di Ruby. Non ci sentivamo più dipendenti dal fallimento dei test di altri team e abbiamo assunto più direttamente la proprietà dei test E2E per le nostre funzionalità nei nostri repository.
- Abbiamo apprezzato l'opzione di test cross-browser . Con WebdriverIO abbiamo potuto avviare test su diverse funzionalità o browser come Chrome, Firefox e IE, anche se ci siamo concentrati principalmente sull'esecuzione dei nostri test su Chrome poiché oltre l'80% dei nostri utenti ha visitato la nostra app tramite Chrome.
- Abbiamo intravisto la possibilità di integrarci con servizi di terze parti . La documentazione di WebdriverIO ha spiegato come integrarsi con servizi di terze parti come BrowserStack e SauceLabs per aiutare a coprire la nostra app su tutti i dispositivi e browser.
- Abbiamo avuto la flessibilità di scegliere il nostro test runner, reporter e servizi . WebdriverIO non era prescrittivo su cosa usare, quindi ogni team si è preso la libertà di decidere se usare o meno cose come Mocha and Chai o Jest e altri servizi. Questo potrebbe anche essere interpretato come una truffa poiché i team hanno iniziato a spostarsi l'uno dall'altro e ci è voluto molto tempo per sperimentare ciascuna delle opzioni tra cui scegliere.
- L'API WebdriverIO, la CLI e la documentazione erano sufficientemente utili da scrivere test e integrarsi con Docker e CIC D. Potremmo avere molti file di configurazione diversi, raggruppare le specifiche, eseguire test tramite la riga di comando e scrivere test seguendo il modello dell'oggetto della pagina. Tuttavia, la documentazione potrebbe essere più chiara e abbiamo dovuto scavare in molti bug strani. Tuttavia, siamo stati in grado di convertire i nostri test dalla soluzione Ruby Selenium.
Abbiamo fatto progressi in molte aree che ci mancavano nella precedente soluzione Ruby Selenium, ma abbiamo riscontrato molti ostacoli che ci hanno impedito di andare all-in con WebdriverIO, come i seguenti:
- Poiché WebdriverIO era ancora basato su Selenium , abbiamo riscontrato molti strani timeout, arresti anomali e bug, che ci hanno ricordato i flashback negativi con la nostra vecchia soluzione Ruby Selenium. A volte i nostri test si bloccano del tutto quando selezioniamo molti elementi sulla pagina e i test vengono eseguiti più lentamente di quanto vorremmo. Abbiamo dovuto trovare soluzioni alternative per molti problemi di Github o evitare determinate metodologie durante la scrittura dei test.
- L'esperienza complessiva dello sviluppatore non è stata ottimale . La documentazione ha fornito una panoramica di alto livello dei comandi ma non abbastanza esempi per spiegare tutti i modi per usarla. Abbiamo evitato di scrivere test E2E con Ruby e finalmente siamo riusciti a scrivere test in JavaScript o TypeScript, ma l'API WebdriverIO era un po' confusa da affrontare. Alcuni esempi comuni sono stati l'uso di
$
vs.$$
per elementi singolari e plurali,$('...').waitForVisible(9000, true)
per attendere che un elemento non sia visibile e altri comandi non intuitivi. Abbiamo riscontrato molti selettori traballanti e abbiamo dovuto esplicitamente$(...).waitForVisible()
per tutto. - I test di debug sono stati estremamente dolorosi e noiosi per sviluppatori e QA. Whenever tests failed, we only had screenshots, which would often be blank or not capturing the right moment for us to deduce what went wrong, and vague console error messages that did not point us in the right direction of how to solve the problem and even where the issue occurred. We often had to re-run the tests many times and stare closely at the Chrome browser running the tests to hopefully put things together as to where in the code our tests failed. We used things like
browser.debug()
but it often did not work or did not provide enough information. We gradually gathered a bunch of console error messages and mapped them to possible solutions over time but it took lots of pain and headache to get there. - WebdriverIO tests were tough to set up with Docker . We struggled with trying to incorporate it into Docker as there were many tutorials and ways to do things in articles online, but it was hard to figure out a way that worked in general. Hooking up 2 to 3 services together with all these configurations led to long trial and error experiments and the documentation did not guide us enough in how to do that.
- Choosing the test runner, reporter, assertions, and services demanded lots of research time upfront . Since WebdriverIO was flexible enough to allow other options, many teams had to spend plenty of time to even have a solid WebdriverIO infrastructure after experimenting with a lot of different choices and each team can have a completely different setup that doesn't transfer over well for shared knowledge and reuse.
To summarize our WebdriverIO and STUI comparison, we analyzed the overall developer experience (related to tools, writing tests, debugging, API, documentation, etc.), test run times, test passing rates, and maintenance as displayed in this table:
Moving On to Cypress
At the end of the day, our WebdriverIO tests were still flaky and tough to maintain. More time was still spent debugging tests in dealing with weird Selenium issues, vague console errors, and somewhat useful screenshots than actually reaping the benefits of seeing tests fail for when the backend or frontend encountered issues.
We appreciated cross-browser testing and implementing tests in JavaScript, but if our tests could not pass consistently without much headache for even Chrome, then it became no longer worth it and we would then simply have a STUI 2.0.
With WebdriverIO we still strayed from the crucial aspect of providing a way to write consistent, debuggable, maintainable, and valuable E2E automation tests for our frontend applications in our original goal. Overall, we learned a lot about integrating with Buildkite and Docker, using page objects, and outlining tests in a structured way that will transfer over to our final solution with Cypress.
If we felt it was necessary to run our tests in multiple browsers and against various third-party services, we could always circle back to having some tests written with WebdriverIO, or if we needed something fully custom, we would revisit the STUI solution.
Ultimately, neither solution met our main goal for E2E tests, so follow us on our journey in how we migrated from STUI and WebdriverIO to Cypress in part 2 of the blog post series.