Frontend testing tips

Questo articolo ha come obiettivo conoscere ed approfondire con alcuni casi concreti riscontrati nello sviluppo quotidiano il testing funzionale di applicazioni Angular 2 eseguito tramite Protractor.

Il test funzionale detto anche E2E (end to end) consiste nel verificare la funzionalità completa di un’applicazione. Al contrario di quello unitario che si prefigge il test di funzionalità atomiche, il test E2E ha come obiettivo verificare che tutte funzionalità lavorino correttamente assieme.

Lo strumento principale utilizzato con Angular è Protractor, un framework di test E2E che viene integrato in applicazioni generate con Angular cli.

Le basi per iniziare

Questo capitolo è una guida introduttiva per un quick setup, che ci porterà alla scrittura del primo test che approfondiremo nei capitoli successivi.

Repository di riferimento

Un esempio funzionante del progetto angular base su cui  girano i test di cui parleremo è reperibile presso: https://github.com/mzuccaroli/angular-cli-tests-example, si tratta di un semplice progetto angular2 generato con angular CLI col comando “ng new”. Per maggiori informazioni vedere il quickstart di un progetto angular2.

Test funzionali o E2E

I test E2E sono progettati per garantire che l’applicazione si comporti come previsto dal punto di vista dell’utente, inoltre, i test non riguardano l’effettiva implementazione del codice ma solo la sua funzionalità. Nel contesto di applicazioni web, come quelle realizzate con Angular, questo significa dover interagire in maniera automatica con browser, click, touch e tempi di caricamento.

frontend testing tipsSi tratta di un testing di alto livello, di una granularità del tutto diversa di quello tipico del TDD, per sua natura più lento e difficile da scrivere di quello unitario; è normale quindi avere una quantità contenuta di test funzionali a fronte di un alto numero di test unitari.

Solitamente i test e2e rispecchiano i test fatti “manualmente” per verificare il corretto funzionamento di interi flussi di un’applicazione e, nel contesto dello sviluppo frontend, sono uno strumento essenziale per garantire il funzionamento cross-browser e cross-platform, ci danno la possibilità di verificare in maniera automatica che la nostra applicazione funzioni sui browser che abbiamo scelto di supportare.

Un esempio tipico di test E2E può essere rappresentato dal flusso di registrazione utente: partendo dall’homepage si clicca sul bottone “registrati” si compila il form, si esegue la validazione dei dati inseriti, si esegue il submit del form e magari successivamente si verifica che l’utente appena registrato riesca ad eseguire il login.

 

Protractor

Protractor è framework di test E2E che consente di testare applicazioni frontend su un browser reale simulando le interazioni nel modo in cui un utente reale si comporterebbe con esso.

Protractor Logo

Protractor gira su Selenium WebDriver, che è un’API per l’automazione e testing su browser, al quale aggiunge funzionalità per interagire con i componenti UI di un’applicazione Angular.

Quickstart con Protractor e Angular CLI

Angular CLI fornisce in maniera predefinita due framework per il testing:

  • testing unitario basato su Jasmine e Karma
  • testing E2E basato su Protractor

L’applicazione base viene fornita con un test di esempio già pronto e per eseguirlo è sufficiente eseguire il comando:

$ ng e2e

Tutti i test con estensione ‘.e2e-spec.ts’ presenti nella cartella e2e situata nella root del nostro progetto saranno eseguiti in sequenza.

Nella console vedremo un risultato del genere:

** NG Live Development Server is listening on localhost:49152, open your browser on http://localhost:49152/ **
Date: 2018-06-14T08:37:09.680Z
Hash: 7178076d4c3f42891352
Time: 9436ms
chunk {inline} inline.bundle.js, inline.bundle.js.map (inline) 5.83 kB [entry] [rendered]
chunk {main} main.bundle.js, main.bundle.js.map (main) 2.31 MB {inline} [initial] [rendered]
chunk {polyfills} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 221 kB {inline} [initial] [rendered]
chunk {styles} styles.bundle.js, styles.bundle.js.map (styles) 11.3 kB {inline} [initial] [rendered]

webpack: Compiled successfully.
[10:37:11] I/update - chromedriver: unzipping chromedriver_2.40.zip
[10:37:11] I/update - chromedriver: setting permissions to 0755 for /Users/myuser/workspace/angular-cli-single-tests-example/node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.40
[10:37:11] I/launcher - Running 1 instances of WebDriver
[10:37:11] I/direct - Using ChromeDriver directly...
Jasmine started

 angular-cli-single-tests-example App
   ✓ should display welcome message

Executed 1 of 1 spec SUCCESS in 0.595 sec.
[10:37:14] I/launcher - 0 instance(s) of WebDriver still running
[10:37:14] I/launcher - chrome #01 passed

Potremo notare che un’istanza di chrome verrà velocemente aperta e richiusa, si tratta del browser reale con cui Protractor e a sua volta Selenium interagiscono:

Protractor browser

Vedremo dettagliatamente in seguito tutti i test che vengono eseguiti sul nostro codice di riferimento.

Quickstart senza Angular CLI

Per installare Protractor senza basarsi su Angular CLI seguire la seguente procedura,

lanciare il comando

$ npm install -g protractor

Configurare il server Selenium: sarà necessario creare un file con le configurazioni necessarie per far girare i test come timeout, directory dei sorgenti ecc..

Creiamo quindi il file tests.conf.js.

exports.config = {  
   seleniumAddress: 'http://localhost:4444/wd/hub',  
   getPageTimeout: 60000,  
   allScriptsTimeout: 500000,  
     
   specs: ['*.e2e-spec.js'],  
 
   suites: {  
       suite1: 'spec/suite1/*.e2e-spec.js',  
       suite2: 'spec/suite2/*.e2e-spec.js'  
   }, 
 
   baseURL: 'http://localhost:8080/',  
   framework: 'jasmine',  
};

Lanciare il server Selenium: in un altro terminale aggiorniamo e lanciamo il server Selenium ci cui avremo bisogno per far girare protractor:

$ webdriver-manager update  
$ webdriver-manager start

A questo punto sarà possibile eseguire i nostri test lanciando il comando:

$ protractor tests.conf.js

Primo test di esempio

Un primo test di esempio ridotto al minimo è quello che segue:

import {browser, by, element} from 'protractor';

describe('angular-cli-single-tests-example App', () => {

   it('should display welcome message', () => {
       browser.get('/');
       expect(element(by.css('app-root h1')).getText()).toEqual('Welcome to app!');
   });

});

sarà sufficiente inserire questo codice nel file ‘basicExample.e2e-spec.ts’ nella cartella e2e e lanciare il comando per eseguire il test:

$ ng e2e --specs=e2e/basicExample.e2e-spec.ts

Chi ha già confidenza con i test unitari e jasmine non farà fatica a scrivere test del genere, molto semplicemente all’avvio del test verrà caricata la pagina principale della nostra applicazione (  ‘/ ‘) nel browser di riferimento e verrà verificata la presenza del contenuto testuale ‘Welcome to app!’ nell’elemento identificato dal selettore css ‘app-root h1’.

Si tratta naturalmente di un test di esempio utile comunque a prendere confidenza con l’ambiente protractor e Angular CLI, ci sono molte metodologie per fare test più approfonditi che vedremo in seguito.

Da notare come al contrario dei test unitari eseguiti con Karma (abbiamo parlato di questo inconveniente in questo articolo ) è possibile eseguire un test specifico specificando il parametro ‘–spec’:

$ ng e2e --specs=path/to/myTest.ts

mentre con

$ ng e2e

verranno eseguiti tutti i test presenti nella cartella e2e.

Protractor e Page Objects: l’importanza dell’utilizzo di pattern nei test

Protractor offre un ottimo sistema di testing perfettamente integrato con Angular, ma così come l’abbiamo presentato nel test precedente porta con sé degli evidenti problemi.

L’obiettivo è identificare e evitare tutto ciò che renderebbe difficile la manutenzione dei test: la crescita del progetto in dimensioni e complessità li renderebbe di fatto un costo al posto di una risorsa e porterebbe irrimediabilmente al loro abbandono.

Repository di riferimento

Un esempio funzionante del progetto angular base su cui  girano i test di cui parleremo è reperibile presso: https://github.com/mzuccaroli/angular-cli-tests-example, si tratta di un semplice progetto angular2 generato con angular CLI col comando “ng new”. Per maggiori informazioni vedere il quickstart di un progetto angular2.

Principali problemi di un approccio non strutturato al testing con protractor:

Prendiamo come esempio questo test che esegue il login e verifica la presenza di un messaggio di benvenuto:

import {browser, by, element} from 'protractor';

describe('angular-cli-single-tests-example App', () => {

  it('should display welcome message', () => {
    browser.get('/login');

    element(by.css('#registration > div > div > form > div.gform_body > input.username').sendKeys('testUser');
    element(by.css('#registration > div > div > form > div.gform_body > input.password').sendKeys('testPassword');

    element(by.css('#registration > div > div > form > div.gform_footer.top_label > button').click();

    expect(element(by.css('app-root h1')).getText()).toEqual('Welcome to app testuser!');
  });

});

Oltre alla difficoltà di lettura che ci rende impossibile capire “al volo” qual è l’obiettivo del test si possono notare i seguenti problemi:

  1. Mancanza di supporto ad un DLS (Domain Specific Language): è difficile capire cosa viene testato, perché le strutture tipiche  di Protractor (element, by.binding, by.css, etc.) non sono correlate direttamente alle funzionalità da testare. Avere test che usano lo stesso gergo del dominio dell’applicazione è molto utile per aiutare chi non li ha sviluppati a comprendere le motivazioni alla base dei test.
    Per fare un esempio concreto è molto più semplice comprendere cosa fa un codice del tipo “loginPage.getUsernameInput()” rispetto a un “element(by.css(‘#registration > div > div > form > div > input .username’)”
  2. Duplicazione del codice: si tratta di una diretta conseguenza del punto precedente, se per testare una funzione abbiamo la necessità di cliccare su un bottone varie volte il codice per selezionarlo verrà ogni volta ripetuto ma soprattutto verrà duplicato su più test che incidentalmente devono eseguire un click sullo stesso bottone
  3. Alto accoppiamento: la logica dei test è strettamente legata al codice e ai selettori, cambiamenti marginali sulla struttura del DOM o delle classi, che normalmente accadono nelle fasi avanzate di un progetto, potrebbero rendere necessario riscrivere tutti i test, è essenziale mantenere un basso accoppiamento ed essere abbastanza flessibili per essere pronti al cambiamento

Page Objects: un design pattern dedicato

Una soluzione a questi problemi è l’utilizzo di Page Objects o PO, design pattern introdotto dal Selenium team, volto a migliorare la manutenzione dei test e ridurre la duplicazione del codice. Un Page Object è una classe/oggetto che fa da interfaccia per le pagine dell’applicazione fornendo un’astrazione di questa verso i test.

Nel processo di testing potremo quindi chiamare i metodi del Page Object senza preoccuparci dell’implementazione della pagina.

page object pattern

A fronte di un cambio dell’UI della pagina o della sua implementazione dovremo solo aggiornare il codice del  PO senza doverci occupare dei test che verificano la funzionalità.

Applicare questo pattern risolve i problemi sopracitati:

  1. Supporto ad un DLS: tutti i selettori tipici della pagina o le grammatiche di Protractor possono essere spostati nel PO esponendo solo metodi legati a logica e funzionalità con nomi rilevanti e facili da leggere come: “page.getLoginButton().click()”
  2. Duplicazione del codice: un oggetto esterno che può essere chiamato da più test evita ogni duplicazione di codice permettendoci anche di automatizzare le parti duplicate: se abbiamo la necessità di eseguire molti test come utenti autenticati nella nostra applicazione un semplice “page.doUserLogin()” ci permette di scrivere una volta sola il codice che automatizza il login.
  3. Alto accoppiamento: una funzione del genere ci permette anche di attuare disaccoppiamento, se l’implementazione del form di login cambia dovremo modificare solo la funzione “doUserLogin()” senza toccare i test

Seguendo questo pattern test di esempio potrà essere rifattorizzato nella seguente maniera:

import { AppPage } from './app.po';

describe('angular-cli-single-tests-example App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
    page.navigateToLogin();
    page.doUserLogin()
  });

 it('should display welcome message', () => {
   expect(page.getWelcomeText()).toEqual('Welcome to app testuser!');
  });
});

Mentre la tutta la logica legata al DOM e al browser viene spostata nel nuovo PO:

import { browser, by, element } from 'protractor';
import { browser, by, element } from 'protractor';

export class AppPage {
  navigateToLogin() {
  return browser.get('/login');
}

doUserLogin(){
  element(by.css('#registration > div > div > form > div.gform_body > input.username').sendKeys('testUser');
  element(by.css('#registration > div > div > form > div.gform_body > input.password').sendKeys('testPassword');
  element(by.css('#registration > div > div > form > div.gform_footer.top_label > button').click();
}

getWelcomeText() {
  return element(by.css('app-root h1')).getText();
}

Da notare che, anche in questo caso basilare, se il testo da verificare viene spostato da un h1 a un altro tipo di elemento html oppure la route della pagina viene modificata da “/login” a “/home” va aggiornato solo il PO mentre il  test rimane intatto e la logica funzionale non viene in nessun modo intaccata.

Degli esempi reali seppur più semplici sono reperibili nella repository di riferimento, più precisamente un test che non fa uso di page object, un test che ne fa uso e il page object di riferimento.

Testing multibrowser: Protractor e integrazione con Browserstack

Creare test funzionali con Protractor come trattato nei capitoli precedenti va ben oltre i testing fatto durante il normale sviluppo, con poche modifiche ci permette far girare la nostra suite di test direttamente su Browserstack. In questa maniera avremo la possibilità di eseguire interi flussi applicativi  in multipiattaforma e garantendo quindi la funzionalità cross-browser della nostra applicazione.

Repository di riferimento

Un esempio funzionante del progetto angular base su cui  girano i test di cui parleremo è reperibile presso: https://github.com/mzuccaroli/angular-cli-tests-example, si tratta di un semplice progetto angular2 generato con angular CLI col comando “ng new”. Per maggiori informazioni vedere il quickstart di un progetto angular2.

Browserstack

Browserstack

Un altro pre-requisto è l’accesso a un account browserstack con piano automate, si tratta di un servizio a pagamento ma, una volta configurato a dovere, ci permette di eseguire contemporaneamente su più browser e su più piattaforme i nostri test funzionali, risparmiando di fatto ore di scomodo testing “manuale”.

BrowserStack logo

BrowserStack è una piattaforma per il testing web e mobile basata su cloud che consente agli sviluppatori di testare siti web e applicazioni su browser, sistemi operativi e dispositivi mobili reali, senza richiedere agli utenti di installare o gestire internamente una serie di macchine virtuali, dispositivi ed emulatori.

Vale comunque il principio che le stesse metodologie possono essere applicate con altri tipi di server selenium che potremo gestire autonomamente, indubbiamente BrowserStack è la soluzione più “comoda”.

Come fare

L’idea è quella di creare una nuova configurazione di Protractor dedicata a browserstack da avviare all’occorrenza, vediamo come fare.

Partendo dalla configurazione base che ci fornisce angular-cli creiamo il nuovo file protractor.browserstack.conf.js

$ cp protractor.conf.js protractor.browserstack.conf.js

Aggiungiamo l’indirizzo del server selenium esposto da browserstack

'seleniumAddress': 'http://hub-cloud.browserstack.com/wd/hub',

Aggiungiamo ora le nuove sezioni “commoncapabilities” e “multicapabilities” dove potremo specificare le nostre credenziali browserstack e quali browser vogliamo utilizzare per i test

commonCapabilities: {
 'browserstack.user': 'xxxx', // TODO: insert user
 'browserstack.key': 'xxxx', // TODO: insert key
 'browserstack.local': true
},

'multiCapabilities': [
 {
  'browserName': 'Chrome'
 },
 {
  'browserName': 'Safari',
  'browser_version': '8'
 },
 {
  'browserName': 'Firefox',
  'browser_version': '50'
 },
 {
  'browserName': 'IE',
  'browser_version': '11.0'
 }
],

Per eseguire i test in parallelo sui vari browser e velocizzare il processo aggiungiamo in coda al file

exports.config.multiCapabilities.forEach(function(caps){
 for(var i in exports.config.commonCapabilities) caps[i] = caps[i] || exports.config.commonCapabilities[i];
});

A questo punto sarà sufficiente eseguire protractor con il seguente comando

$ ./node_modules/.bin/protractor protractor.browserstack.conf.js

Potremo vedere i risultati dei test a console ma potremo anche (a seconda delle configurazione del nostro account browserstack) visualizzare risultati e registrazioni filmate dei nostri test all’indirizzo: https://automate.browserstack.com/

Testing su server privati

Spesso ci capiterà che l’applicazione che vogliamo testare non sia raggiungibile dalla rete pubblica, è una situazione comune per ambienti di staging o per applicazioni che girano su reti private. Browserstack-local ci viene incontro in questa evenienza permettendoci di eseguire local binding dalla nostra macchina di sviluppo.

Installiamo blowserstack-local

npm install browserstack-local

Poi adattiamo la nostra configurazione importandolo e aggiungendo le chiamate beforeLaunch e afterLaunch

var browserstack = require('browserstack-local');
// Code to start browserstack local before start of test
beforeLaunch: function(){
 console.log("Connecting local");
 return new Promise(function(resolve, reject){
  exports.bs_local = new browserstack.Local();
  exports.bs_local.start({'key': exports.config.capabilities['browserstack.key'] }, function(error) {
   if (error) return reject(error);
   console.log('Connected. Now testing...');

   resolve();
  });
 });
},

// Code to stop browserstack local after end of test
afterLaunch: function(){
 return new Promise(function(resolve, reject){
  exports.bs_local.stop(resolve);
 });
}

è importante ricordarsi di abilitarlo all’interno delle commoncapabilities

'browserstack.local': true,

a questo punto potremo lanciare senza problemi test su server privati con tutti i benefici che ci può fornire browserstack.

Da notare che l’utilizzo di browserstack locale rallenta un po l’esecuzione dei test, è molto probabile che ci troveremo a dover ritoccare un po i timeout, sia di protractor che di browserstack, per evitare che i nostri test falliscano, soprattutto quelli che girano su Internet Explorer.

un esempio completo di quanto trattato è reperibile sulla nostra repository di riferimento: protractor.browserstack.conf.js.

Il risultato finale sarà un file simile al seguente:

const {SpecReporter} = require('jasmine-spec-reporter');
var browserstack = require('browserstack-local');

exports.config = {
    allScriptsTimeout: 50000,
    specs: [
        './e2e/**/*.e2e-spec.ts'
    ],

    'seleniumAddress': 'http://hub-cloud.browserstack.com/wd/hub',

    commonCapabilities: {
        'browserstack.user': 'xxxx', // TODO: insert user
        'browserstack.key': 'xxxx', // TODO: insert key
        'browserstack.local': true
    },

    'multiCapabilities': [{
        'browserName': 'Chrome'
    }, {
        'browserName': 'Safari',
        'browser_version': '8'
    }, {
        'browserName': 'Firefox',
        'browser_version': '50'
    }, {
        'browserName': 'IE',
        'browser_version': '11.0'
    }],

    baseUrl: 'http://www.testpage.demo/',
    framework: 'jasmine',
    jasmineNodeOpts: {
        showColors: true,
        defaultTimeoutInterval: 50000,
        print: function () {
        }
    },
    onPrepare() {
        require('ts-node').register({
            project: 'e2e/tsconfig.e2e.json'
        });
        jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
    },

    // Code to start browserstack local before start of test 
    beforeLaunch: function () {

        console.log("Connecting local");
        return new Promise(function (resolve, reject) {
            exports.bs_local = new browserstack.Local();
            exports.bs_local.start({'key': exports.config.capabilities['browserstack.key']}, function (error) {
                if (error) return reject(error);
                console.log('Connected. Now testing...');

                resolve();
            });
        });
    },

    // Code to stop browserstack local after end of test
    afterLaunch: function () {
        return new Promise(function (resolve, reject) {
            exports.bs_local.stop(resolve);
        });
    }
};

// Code to support common capabilities
exports.config.multiCapabilities.forEach(function (caps) {
    for (var i in exports.config.commonCapabilities) caps[i] = caps[i] || exports.config.commonCapabilities[i];
});

Test su tab multiple

Nell’ambito dello sviluppo di test funzionali capita di dover verificare l’apertura di nuove tab nel browser ed eseguire i test da lì.

Oltre a dover gestire la nuova tab è necessario assicurarsi che una volta completato il test le tab aperte vengano richiuse e la situazione venga ripristinata per non creare problemi ai test successivi.

frontend testing tips

Repository di riferimento

Un esempio funzionante del progetto angular base su cui  girano i test di cui parleremo è reperibile presso: https://github.com/mzuccaroli/angular-cli-tests-example, si tratta di un semplice progetto angular2 generato con angular CLI col comando “ng new”. Per maggiori informazioni vedere il quickstart di un progetto angular2.

Protractor multiple tabs

Ecco un breve esempio di come ho risolto il problema basandomi sulle funzioni di windowhandle dell’oggetto browser fornito fa Protractor

Come fare

Nota: questo esempio fa uso di page object.

Creiamo un nuovo test

$ touch e2e/multipleTabs.e2e-spec.ts

La versione più “semplice” di questo test (che successivamente rifattorizzeremo nel page object) è la seguente:

import {AppPage} from './app.po';
import {browser} from 'protractor';

describe('angular-cli-tests-example App', () => {
 let page: AppPage;

 it('should open a new tab with app homepage after clicking on "newtab" link', () => {
  page.navigateTo();
  page.getNewTabLink().click();

  browser.getAllWindowHandles().then(function (handles) {
   expect(handles.length).toEqual(2);
   browser.switchTo().window(handles[1]).then(function () {
    expect(page.getParagraphText()).toEqual('Welcome to app!');
   });
  });
 });
});

la parte rilevante è browser.getAllWindowHandles() che restituisce di fatto un array con le tab e la chiamata browser.switchTo().window(handles[1]) che fa cambiare tab al nostro browser di test dandoci la certezza di testare gli expect successivi nella tab desiderata

siamo pronti per eseguire la prima versione del test con

$ ng e2e --specs=e2e/multipleTabs.e2e-spec.ts

non vogliamo lasciare tab aperte dopo l’esecuzione di questo test e per sicurezza vogliamo accertarci che anche prima dell’esecuzione del test singolo non ci siano altre tab aperte, magari lasciate da test precedenti, ad interferire con quelle che vogliamo verificare, scriviamo quindi la seguente funzione che per comodità potremo aggiungere ad una nuova classe TestUtils

closeOpenedTabs() {
 browser.getAllWindowHandles().then(function (handles) {
  for (let i = 1; i < handles.length; i++) {
   if (handles[i]) {
    console.log('** testutils - closing tab: ' + i + ' **');
    browser.driver.switchTo().window(handles[i]);
    browser.driver.close();
   }
  }
 browser.driver.switchTo().window(handles[0]);
 });
}

questa funzione potrà essere chiamata nei afterEach e beforeAll per assicurarci di avere controllo pieno sullo stato delle tab, dopo un po di refactoring, spostando le funzioni riutilizzabili in TestUtils e modificando il resto per rispettare il pattern Page Object il risultato finale sarà il seguente:

describe('angular-cli-tests-example App', () => {
 let page: AppPage;

 beforeAll(() => {
  page = new AppPage();
  testUtils.closeOpenedTabs();
 });

 afterEach(() => {
  testUtils.closeOpenedTabs();
 });

 it('should open a new tab with app homepage after clicking on "newtab" link', () => {
  page.navigateTo();
  page.getNewTabLink().click();

  page.getOpenedTabs().then(function (tabs) {
   expect(tabs.length).toEqual(2);
   testUtils.goToTab(tabs[1]).then(function () {
    expect(page.getParagraphText()).toEqual('Welcome to app!');
   });
  });
 });
});

Come sempre un esempio funzionante è reperibile nella repository di riferimento, in dettaglio il page object e il test su tab multiple.

Quick tip: acquisire screenshot al fallimento dei test

Nell’ambito dei test funzionali eseguiti con protractor, potremmo avere difficoltà a capire cosa è andato storto quando un test fallisce, soprattutto quando facciamo girare grosse suite.

Se si tiene conto della lentezza che spesso contraddistingue il testing e2e e della delicatezza dell’ambiente di testing risulta molto utile la possibilità di acquisire screenshot dei test che falliscono.

Repository di riferimento

Un esempio funzionante del progetto angular base su cui  girano i test di cui parleremo è reperibile presso: https://github.com/mzuccaroli/angular-cli-tests-example, si tratta di un semplice progetto angular2 generato con angular CLI col comando “ng new”.

Se vogliamo salvare screenshot durante il processo di testing locale con Protractor l’oggetto browser ci viene incontro dandoci la possibilità di acquisire screenshot in qualsiasi momento vogliamo: prima durante o dopo un test e soprattutto potremo farlo solo se un test fallisce. Probabilmente una rapida occhiata allo allo stato dell’applicazione nel momento del fallimento del test ci permetterà di individuare al volo problematiche che ci avrebbero portato via un sacco di tempo in debug manuale.

frontend testing tips

Come Fare

Come nei capitoli precedenti faremo riferimento a un ambiente di test creato appositamente ma la soluzione è applicabile a tutti gli ambienti.

Innanzitutto installiamo le librerie che ci permetteranno di scrivere sul nostro file system

$ npm install file-system --save

ora creiamo una cartella che ospiterà i risultati dei nostri test e tramite un semplice file .gitkeep la aggiungiamo alla nostra codebase

$ mkdir e2e/testresults
$ touch e2e/testresults/.gitkeep

vogliamo che la cartella testresults sia sempre presente ma non vogliamo rischiare di aggiungere a commit i nostri screenshot, che potrebbero occupare anche parecchio spazio, quindi ricordiamoci di aggiungerli al nostro .gitignore

# e2e
/e2e/*.js
/e2e/*.map
/e2e/testresults/*.png

Ora andiamo a creare dentro il file testUtils.ts (che abbiamo già usato nel capitolo su come gestire tab multiple) la seguente funzione:

public takeFailScreens() {

   browser.takeScreenshot().then(function (png) {
       jasmine.getEnv().addReporter(new function () {
           this.specDone = function (result) {
               let filename = Date.now() + result.fullName.replace(/ /g, '_') + '.png';
               if (result.failedExpectations.length > 0) {
                   filename = 'FAIL_' + filename;
                   const screenShotDirectory = 'e2e/testresults/';
                   const stream = fs.createWriteStream(screenShotDirectory + filename);

                   stream.write(new Buffer(png, 'base64'));
                   stream.end();
               } else {
                   // SUCCESS TEST
               }
           };
       });

   });
};

Da notare come i rami if ed else della condizione (result.failedExpectations.length > 0) rappresentano rispettivamente la situazione di test fallito e test passato; se volessimo fare operazioni anche in caso di test riusciti potremmo aggiungere codice  in corrispondenza del commento // SUCCESS TEST

Abbiamo scelto di dare allo screenshot un nome che è la concatenazione della data attuale e del nome del test appena fallito, anche in questo caso si tratta di un naming del tutto discrezionale ma utile a trovare le screenshot già organizzate in ordine cronologico e per nome test.

A questo punto, all’interno dei nostri test, sarà sufficiente importare TestUtils se non lo abbiamo già a disposizione e chiamare la funzione  takeFailScreens() nella afterEach, va da se che questa è una delle tante implementazioni possibili e con le stesse metodologie possiamo fare moltissime cose: salvare screenshot in altri momenti del testing o anche in caso di successo.

Il nostro test base dopo le modifiche avrà un aspetto simile:

import {TestUtils} from './testUtils';
import {AppPage} from './app.po';

const testUtils = new TestUtils();

describe('angular-cli-tests-example App', () => {
   let page: AppPage;

   beforeEach(() => {
       page = new AppPage();
   });

   afterEach(testUtils.takeFailScreens);

   it('should display welcome message', () => {
       page.navigateTo();
       expect(page.getParagraphText()).toEqual('Welcome to app!');
   });
});

Possiamo provare a far fallire i test di proposito aggiungendo un “expect(false).toEqual(true)”  per visualizzare nella cartella e2e/testresults lo screenshot relativo come per l’esempio che segue.

protractor fail screenshot

1 commento

Trackbacks & Pingbacks

  1. […] This post is a part of the frontend testing series on Team Scaling Blog. Read the original Italian version here. […]

Lascia un Commento

Vuoi partecipare alla discussione?
Fornisci il tuo contributo!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *