Frontend testing tips

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.

Note

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

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 prenza 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.

 

0 commenti

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 *