User Acceptance Test (UAT) e UI Testing

Nell’intraprendere una nuova collaborazione, vediamo che le principali motivazioni del cliente sono correlate alla risoluzione di un problema o alla soddisfazione di un bisogno. Noi, in quanto Team di Sviluppo, ci prodighiamo a capire le cause che spingono il cliente ad identificare la sua situazione attuale come bisogno o problema ed intraprendiamo con lui quella strada lunga e tortuosa, che si conclude nello stilare i requisiti funzionali che la nostra soluzione (per noi tipicamente software) dovrà garantire. Essendo questo un processo complesso, ed essendo il cliente spesso audace ma al contempo indeciso, può capitare che quello che abbiamo implementato per lui, a partire dai requisiti funzionali, non sia esattamente quello che desiderava. Tecniche Agile come Scrum e Kanban possono ridurre al minimo questo tipo di problematica, garantendo demo e rilasci molto frequenti in modo da cambiare la rotta di sviluppo in un tempo accettabile, tuttavia in questo articolo affronteremo una tecnica che ci aiuta a centrare il desiderio del cliente (da qui in avanti anche detto Team di Business) tramite la definizione di criteri di accettazione (Acceptance Criteria, AC) confermati dagli User Acceptance Test (UAT). Abbiamo dunque bisogno della definizione degli UAT per possedere una metodologia di test formale, definire gli AC e centrare i desideri del cliente, piuttosto che basare il nostro sviluppo esclusivamente su una documentazione ricevuta ad inizio progetto. Se la soluzione software si rivelasse essere di grosse dimensioni e durata di sviluppo, inoltre, i Business Requirements (BR) del cliente potrebbero variare in corso d’opera rispetto a quelli inizialmente stabiliti.

Non entreremo nel dettaglio della definizione degli UAT, dato che è un argomento che riguarda principalmente il Team di Business, tuttavia vedremo delle tecniche che, se applicate durante la fase di sviluppo, possono supportare in modo non indifferente i test di accettazione.

Una caratteristica particolare degli UAT è che non coinvolgono solo sviluppatore e cliente, bensì l’intero Team di Sviluppo e Team di Business, poiché vanno a toccare l’intero business domain, o per così dire l’intera Supply Chain del client, se questo ne possiede una. Gli stakeholders dunque sono:

  • Sponsor: persona (od insieme di persone) che ha commissionato il lavoro;
  • Manager: responsabile di consegnare i requisiti business a partire dall’implementazione del sistema (in Agile, il Product Owner);
  • Utente finale: colui il quale utilizzerà realmente il sistema;
  • Sviluppatori: responsabili dell’implementazione del sistema, forniscono supporto agli UAT.

Queste figure prenderanno parte alla definizione ed all’esecuzione degli UAT, al fine di cercare il più possibile di evitare le sorti del Chaos Report.

Il Chaos Report è uno studio portato avanti da un gruppo di professionisti dell’ICT chiamato The Standish Group, con lo scopo di collezionare statistiche di casi reali di fallimento di soluzioni IT per migliorarne con il tempo il tasso di successo. Il primo report originale è stato pubblicato nel 1994 e fino al 2015 ne ha mantenuto la stessa forma (oggi i report collocati in quel periodo sono denominati legacy), mentre dal 2016 possiamo trovare un nuovo formato del report. La versione del 1994 esaminò 8380 casi e rilascio le seguenti percentuali:

  • Progetti completati nei costi e nei tempi previsti, contenenti tutte le funzionalità originariamente pattuite – 16%;
  • Progetti completati sforando costi, tempi e contenenti un sottoinsieme di tutte le funzionalità originariamente pattuite – 53%;
  • Progetti cancellati, bloccati o non utilizzati in produzione – 31%.

Il report inoltre prova ad indentificare le cause dei suddetti fallimenti stilando un elenco ordinato secondo probabilità:

  1. Requisiti incompleti;
  2. Mancato coinvolgimento dell’utente finale;
  3. Poche risorse;
  4. Aspettative non realistiche;
  5. Altro;
  6. Mancato supporto dall’esecutivo;

Molte di queste cause possono essere coperte eseguendo uno UAT formare, come vedremo di seguito.

Acceptance Criteria

Per definire dei test di accettazione, dobbiamo gioco forza capire quali sono i criteri da testare, tipicamente definiti dal Team di Business. Un Acceptance Criteria (AC) è dunque la regola o il contratto che una funzionalità, un componente o un sistema deve rispettare affinché venga accettato dal Business. Se i BR ci dicono cosa il nostro sistema deve fare, gli AC ci danno l’idea dello stato dello sviluppo e ci fanno capire se il sistema è pronto o meno per il rilascio. Ovviamente tutto dipende dalla qualità degli AC; un criterio troppo generico rischia di coprire pochi requisiti funzionali e non rispecchiare precisamente i bisogni del Business, oltre a portare ulteriori danni, come non rientrare in costi e tempi o testare funzionalità non ben definite.

Inoltre gli AC hanno un ulteriore beneficio: se alla data di rilascio prevista tutti gli AC non sono stati rispettati, questi possono essere usati per dare una stima del lavoro mancante e della quantità di tempo ulteriore da investire nello sviluppo.

Per gli AC come per i BR, tipicamente la causa principale di inesattezze, errori o forti discostamenti dalle definizioni originali, è la sostanziale differenza di conoscenza verticale fra i vari stakeholders del progetto: sponsor, manager, sviluppatori ed utenti finali hanno tutti descritto cosa il sistema deve fare; tuttavia lo sponsor magari conosce gli ingranaggi del business ma non come funziona tecnicamente, utente finale e manager conoscono cosa il sistema deve fare ma spesso non come funzionerà, mentre lo sviluppatore capisce la tecnologia del sistema ma non il business che c’è dietro.

Quali sono, dunque, i test di accettazione che Team di Sviluppo e Team di Business devono coprire? Sicuramente tutti quelli scaturiti da BR definiti nel contratto firmato fra le parti, insieme a quelli che coprono le aspettative dell’utente; inoltre, dove c’è logica di business ci sono fisiologicamente dei Business Processes (BP), che dovrebbero essere tutti coperti da test. Ci dovrebbe essere, infine, un Test detto “di regressione“, per ogni cambiamento del sistema in corso d’opera, al fine di evitare i fastidiosissimi bug di regressione. Tutto quello che è stato già testato in fase di sviluppo, come unit test, integration test, system test, non dovrà essere coperto da UAT.

Tecniche formali di UAT – User Acceptance Test

Un UAT, per essere considerato tale, deve essere formale, oggettivo e strutturato; la prima caratteristica permette al test di verificare se una funzionalità o un sistema rispettano i requisiti richiesti, ovvero rispettano quello che è diventato lo “standard” per il cliente; inoltre un approccio strutturato permette di garantire che ogni funzionalità abbia almeno un test che la copra. Infine deve essere oggettivo in quanto, se durante la sua esecuzione vengono evidenziate delle problematiche o delle mancanze, deve provvedere ad esporne le prove concrete, in modo da rendere più veloce ed accurato il debugging e la risoluzione. In ambito UAT, infatti, il testing ed il bug fixing sono due attività fortemente disaccoppiate, al contrario di quanto accade normalmente, in quanto il Team di Business, ed in particolare l’utente finale, è tenuto a trovare i bug ma non ad eseguirne un fix, mentre il Team di Sviluppo può risolvere l’errore ma non portare avanti il test.

Come accennato in precedenza, gli UAT sono l’ultima classe di test ad essere eseguita e non comprendono nessuna, o quasi, delle altre classi di test, come unit, integration, etc. Diciamo quasi perché ci sono in realtà due classi di test di cui l’UAT fa uso: il Functional Testing (detto anche Black-Box Testing) e lo Structural Testing (detto anche White-Box Testing).

  • Black-Box Testing: si basa sull’analisi delle specifiche delle funzionalità di un componente o sistema e si divide in due parti: il Design, che, sfruttando i dati derivati dalle specifiche ed analizzando la funzionalità richiesta, genera un Deliverable detto Test Script (che contiene quali output la funzionalità deve generare a partire dagli input) e l’Execution, che, tenendo conto del Test Script, dà in input gli stessi dati forniti in Design all’implementazione della funzionalità, ottenendo così l’output effettivo, da comparare con quello del Test Script.
  • White-Box Testing: si basa sull’analisi della struttura interna del componente o sistema e verifica che il flusso di esecuzione del test copra tutti i possibili casi che la struttura del componente ci mette a disposizione. Possiamo riassumere un White-Box Test come un diagramma di flusso dell’esecuzione della funzionalità, che riporta sia l’happy path che i casi particolari (eccezioni e non).

In fin dei conti in fase di test dobbiamo essere in grado di completare gli UAT nel modo più efficace e efficiente possibile. Per fare ciò la letteratura ci da una mano fornendoci due processi tipici di UAT, il Fundamental Test Process (FTP) ed il Test Development Process (TDP).

L’FTP si struttura in cinque passi e fornisce la sequenza di attività da compiere per completare il test e degli indici per stabilire il successo o meno di quest’ultimo. I cinque passi sono:

  1. Pianificazione, monitoraggio e controllo;
  2. Analisi e design;
  3. Implementazione ed esecuzione;
  4. Valutazione degli AC e generazione di report;
  5. Attività di chiusura del test.

Come è facilmente intuibile, i White-Box e Black-Box tests aiutano a coprire molti di questi punti.

Il TDP invece descrive le meccaniche di generazione di test effettivi che aiutano a raggiungere la copertura dei test all’interno del componente o sistema desiderati negli AC. Questo si compone di tre parti:

  1. Test Condition; un elemento od un evento di un componente o sistema che può essere verificato da uno o più Test Cases. Il suo scopo è di esprimere certi aspetti dei BR in modo da poter costruire sopra specifici test;
  2. Test Case; un insieme di input, precondizioni, output previsti e post condizioni, sviluppati per un particolare Test Condition. Precondizioni e post condizioni specificano lo stato del sistema prima e dopo l’esecuzione del test (analogamente alla programmazione By Contract);
  3. Test Script (detti anche Test Procedure Specification): un Deliverable che specifica una sequenza di azioni per l’esecuzione di un test. I Tester tipicamente avranno una collezione di Test Scripts che li supporterà durante la fase di testing.

Una delle fasi più importanti degli UAT è il design dei Test-Cases, tramite cui, come anticipato, i tester inizieranno e porteranno avanti il loro lavoro. Dei buoni Test-Cases garantiranno efficienza ed efficacia. La letteratura ci suggerisce due principali tecniche di design[3] che in realtà sono molto simili a design pattern, ovvero soluzioni note per problemi ricorrenti:

  1. Equivalence Partitioning (EP): stabilisce quali siano i dati in input al test da verificare perché questo copra per intero la relativa funzionalità, se l’insieme di input è continuo. L’insieme di input può essere suddiviso in sottoinsiemi validi e non validi. L’EP ci dice che basta testare un solo input del sottoinsieme valido, mentre è necessario testare almeno un input di tutti i sottoinsiemi non validi.
    Per esempio, se una entry di una form di login accetta uno username con numero di caratteri <= 20, l’insieme di input va da 0 a max(int). Il sottoinsieme valido è {1,…,20}, i sottoinsiemi non validi sono {0}, {21,…,max(int)}. Secondo l’EP dovremmo testare un caso per il sottoinsieme valido, quindi inserire uno username di 10 caratteri per esempio, ed un caso per ogni sottoinsieme invalido, quindi inserire 0 caratteri o più di 20 caratteri. Questo garantirebbe la completa copertura della funzionalità;
  • Boundary Value Analisys (BVA): prende in analisi i casi limite del problema (per questo si parla di boundary o edge cases), in quanto, secondo letteratura, sono i casi per cui lo stesso Team di Business sbaglia più spesso all’interno dei business processes, e dunque anche il Team di Sviluppo partirà da una base sbagliata. Il BVA sostiene che occorre testare ogni caso limite ed il suo elemento adiacente fuori dal boundary.
    Nell’esempio precedente equivarrebbe a testare la funzionalità con gli elementi di boundary del sottoinsieme dei validi, ossia 1 e 20, ed i loro adiacenti fuori dal boundary, ovvero 0 e 21.

L’applicazione di questi due pattern ci permette di avere a disposizione un buon papier di Test-Cases da consegnare a chi di dovere, ovvero i testers facenti parte del Team di Business.

Infine, insieme alle tecniche di design, un corretto approccio ai test è fondamentale per la loro buona riuscita. La ricca scelta di approcci deriva dal fatto che, alla fine dei giochi, sono gli utenti finali a decretare gli AC, e che i BR e i BP sono spesso incompleti ed a volte errati. Gli utenti finali dunque, avendo esperienze “orizzontali” assai diverse, sono gli unici ad essere davvero oggettivi nei test, in quanto si distaccano da quella conoscenza ed esperienza verticale posseduta dai tecnici che può invalidare il risultato del test;

Ci sono molti approcci, ma i più famosi sono i seguenti (che si basano sugli aspetti fondamentali degli uat): BR, BP e AC, in cui tutti vanno a generare Test-Cases:

  • Requirements-based: questo approccio permette di generare i Test-Cases direttamente a partire dai BR e tipicamente viene adottato alla fine dello sviluppo del software, durante la fase di preparazione degli UAT. Tuttavia spesso questi Test-Cases sono definiti già al completamento dei BR, dunque all’inizio del ciclo di vita del software; questo comporta possibili informazioni mancanti o deprecate;
  • Business process-based: servono ad essere sicuri che il Sistema lavorerà specificatamente in supporto ai BP ed alla Supply Chain aziendale, oltre a garantire che i BR siano stati soddisfatti, in modo tale da rispecchiare come l’azienda userà il sistema;
  • User interface-driven: basati sull’inserimento di informazione da parte dell’utente e l’interazione di quest’ultimo con schermo e report. Il tester si troverà di fronte ad uno scenario reale e dovrà compiere azioni che saranno reali nel sistema consegnato. Possono essere inclusi dei Business process-based.
  • Risk-based: sono scelti quando la consegna del sistema è in ritardo ed i test devono essere fatti in un lasso di tempo molto ristretto fra la consegna e l’entrata in produzione. Si utilizzano delle regole di priorità per cui verranno eseguiti prima i testi più importanti.

Dunque dopo aver ottenuto un forbito bagaglio di Test-Cases, il Team di Business potrà iniziare a testare le funzionalità ed i moduli del sistema da noi sviluppato. E a questo proposito, noi sviluppatori? Siamo una mera parte passiva nell’ambito degli UAT?
Proseguendo vedremo come una tecnica ed un meccanismo possano decorare e completare gli UAT al fine di supportare l’intero Team di Business, oltre che portare dei grossi vantaggi allo sviluppo.

Una tecnica UAT in ambito sviluppo

Una delle metodologie per lo sviluppo dei test di accettazione, associata alla famosa tecnica del TDD, è quella del Outside-In; nata in quel di Londra (detta infatti anche London Style o Double Loop), reclama il fatto che i test siano in grado di definire una sorta di contratto fra il team di sviluppo, capitanato dal Product Owner, ed il Team di Business; sempre che quest’ultimo sia abbastanza intraprendente da essere disposto a leggere qualche riga di codice auto esplicativo.

Questa tecnica presuppone la forma mentis “Outside-In”, ovvero stabilisce che per definire il codice assieme ai test tramite TDD, si debba partire dagli strati più esterni della nostra infrastruttura software, cosa che invece viene lasciata libera nello stile “Classico” di TDD.

Se in un’architettura ben testata abbiamo Test di interfaccia, Unit test, Integration Test e UaT, nell’Outside-In iniziamo definendo gli UaT a partire dai requisiti discussi con il team di Business e supportati da tecniche di mocking, andremo ad eseguire il cosiddetto Double Loop, composto da loop dell’UAT e da n loop dei relativi Unit Test. Come mostrato nella figura seguente, ragioniamo di loop di fallimento e non di sviluppo, in quanto, supponendo di trovarci all’i-esimo ciclo di iterazione, la metodologia prevede di:

  1. Scrivere l’i-esimo test di accettazione, buildare il progetto e far fallire il test (Outside);
  2. Ispezionare gli errori che ci vengono rimarcati dal nostro IDE e mockare i servizi che alla fine delle iterazioni saranno di supporto e complementari alla logica;
  3. Chiamare il servizio che gestisce la logica facendoci aiutare dall’IDE, il quale probabilmente farà lanciare un’eccezione di non implementazione del metodo;
  4. Buildare il progetto e far fallire lo/gli unit test;
  5. Andare a fixare gli unit test con uteriori mock o con implementazioni effettive, in modo da far compilare il progetto e far passare i test;
  6. Rieseguire lo UAT per vederlo compilare;
  7. Rieseguire il loop dal punto 1 al punto 6  al fine di:
    • Definire un nuovo UAT;
    • Rifattorizzare ed estendere uno UAT esistente;

Vediamo un esempio:

Supponiamo di riuscire a scrivere un criterio di accettazione per un requisito stabilito fra il team di sviluppo ed il team di business, il tutto in un linguaggio facilmente comprensibile che usa i prefissi Given-When-Then. Potremmo trovarci davanti ad una situazione simile a quella descritta dalla figura successiva.

Tramite la tecnica di ATDD (Acceptance Test Driven Developement) possiamo trasformare i suddetti requisiti in metodi che fanno da proxy ai nostri servizi, in questo modo.

Come possiamo vedere, il nostro IDE ci aiuta nel rifattorizzare il metodo appena chiamato. Se volessimo fare build del progetto e far eseguire il test, otterremmo banalmente un lancio dell’eccezione NotImplementedException(), quindi possiamo andare avanti ed iniziare a chiamare il servizio “Under Test”. In ATDD vengono esclusi i test di interfaccia, dunque i primi strati ad essere toccati sono quelli mediatori fra interfaccia e logica, come i controller in MVC oppure i ViewModel in MVVM. Nel nostro caso definiamo un ViewModel che gestirà la logica dell’aggiunta del device, con il supporto dei servizi mocked.

Vediamo che l’IDE, nell’aiutarci nel refactor e nella creazione automatica delle classi e dei metodi, ci metterà sempre i paletti di mancata implementazione, che saranno delle piccole milestone verso cui dovremo arrivare per evitare di far fallire il test. In questo momento siamo ancora a livello Outside, infatti se runniamo lo UAT lo vedremo fallire.

A questo punto potremo iniziare l’implementazione del nostro metodo concreto, che farà uno di alcuni servizi dedicati: grazie all’introduzione di due servizi mocked, non ci preoccuperemo inizialmente della loro implementazione e, rispettando la loro interfaccia, li passeremo al nostro view model tramite dependency injection.
Lo unit test seguente fa uso dei servizi mocked, chiama il metodo in test del viewModel e esegue una verify, ovvero verifica che i metodi definiti nel setup del mock siano effettivamente stati chiamati con quel tipo di parametri in ingresso:


Avendo concluso le iterazioni dell’”In”, potremo rieseguire il test estero, ovvero lo UAT e vederlo completare; potremo così continuare le nostre iterazioni fino a coprire i Given-When-Then dati dai requisiti funzionali.

UI testing come supporto agli UAT

Un modo che abbiamo come sviluppatori per supportare gli UAT del team e per completare il giro di test della nostra architettura è l‘utilizzo di test End-To-End (E2E) automatici. Prendendo in esempio il caso delle applicazioni mobile in Xamarin, possiamo utilizzare il framework di test Xamarin.UITest, che permette di eseguire automaticamente test di interfaccia su applicazioni Xamarin.Android e Xamarin.IOS, sfruttando il ben più noto Framework NUnit. I test scritti in Xamarin.UITest interagiscono con l’interfaccia grafica dell’applicazione, simulando l’interazione dell’utente tramite tap, gesture, inserimenti di testo etc.

Dopo l’integrazione di Xamarin nel mondo Microsoft, il framework di test compie un passo in più, ovvero è possibile pubblicare i test sviluppati e testati in locale su App Center, insieme agli apk/ipa delle nostre applicazioni; in questo modo potranno essere eseguiti in cloud su tutta una serie di device diversi fra loro, ovviamente a pagamento.

Per scrivere gli UITest in Xamarin, Microsoft consiglia una metodologia a Loop attuata durante lo sviluppo delle nostre app, ovvero:

  1. Scrivere e testare i test in locale dopo aver definito una funzionalità;
  2. Pubblicarli assieme alle release su App Center;
  3. Correggere eventuali errori scaturiti dal run su App Center;
  4. Ripetere il processo per la prossima funzionalità.

Simulando come anticipato l’interazione dell’utente con il device, gli UITest sono strettamente legati alle viste ed agli elementi grafici che si trovano sullo schermo. Per questo fanno uso di due specifici Set di API che si interfacciano l’un l’altro:

  • Actions: azioni che possono essere scatenate su vista ed elementi grafici (azioni dell’utente);
  • Queries: Richieste posizione di viste ed elementi grafici;

Il tutto basandosi sul paradigma Read-Eval-Print-Loop (REPL). Gli UITest fanno uso dell’interfaccia IApp che definisce le firme dei metodi utilizzati dalle sue implementazioni iOSApp e AndroidApp tramite i quali si interfaccia all’applicazione. Microsoft suggerisce di creare una nuova istanza di IApp per ogni test. Per fare ciò ne creiamo una all’interno del metodo in override SetUp.

Come possiamo vedere, la creazione dell’istanza è di tipo Builder, in cui dobbiamo specificare la device platform (Android o iOS) e il path (assoluto o relativo) del file .apk o .app della nostra applicazione. Per poter testare un’app iOS è necessario possedere un provisioning profile collegato ad un dispositivo fisico, in quanto sul simulatore iOS non è possibile farlo. Cosa diversa avviene per Android, in cui dovremo disabilitare lo Shared Mono Runtime, questo a discapito delle performance di building e distributing della nostra app. Per fare ciò, andiamo nelle impostazioni del progetto di Android, nella sezione Android Build -> General e togliamo il flag “User Shared Mono Runtime” e dare i permessi per l’utilizzo di internet all’app tramite la stringa <uses-permission android:name=”android.permission.INTERNET” /> nel manifest.

Successivamente creiamo il metodo di UI Test per coprire la nostra funzionalità in via di sviluppo, esattamente come faremmo per uno Unit o Integration test con NUnit.

Come possiamo vedere, l’oggetto _sut (System Under Test) contiene l’implementazione di IApp e offre metodi Actions e Queries come Tap() e Query() che permettono di interagire con la UI e di richiederne lo stato, oltre a darci la possibilità di scattare screenshots.
Infine testiamo il risultato tramite una classica assert.

In questo modo, eseguendo i test da noi scritti, Xamarin.UiTest troverà l’apk (o .app in caso di iOS) dal path che gli abbiamo fornito, lo installerà nel dispositivo disponibile fisico (per iOS) o emulato ed eseguirà il test automatizzato.

Xamarin.UITest ci offre un altro paio di tecniche per ispezionare la vista, ovvero il REPL ed il Marked:

  • REPL: serve ad invocare il REPL di Xamarin.UITest tramite una chiamata al medoto Repl() di IApp; in questo modo verrà aperta una finestra di console in cui sarà presente una variabile app da chiamare per ispezionare la vista. Per esempio, con il metodo tree, il REPL ci ritornerà l’albero della vista dell’applicazione, dandoci modo di capire com’è fatta e di pensare a dei metodi Query sui test che faremo.
  • Marked: è un metodo query che permette di ispezionare la vista andando a cercare proprietà di elementi grafici che sono settate al valore passato nel metodo.
    Per esempio, avendo un bottone definito in Xamarin.Android come segue, potremmo andare a fare query sulla proprietà Text del bottone, eseguendo i metodi
    app.Query(c=>c.Marked(“action1_button”));
    app.Query(c=>c.Marked(“Action 1”));
    Il primo ci restituirà l’id del bottone, il secondo il testo.

Dunque, con un approccio agile allo sviluppo del software, tramite la tecnica del ATDD ed un rinforzo agli edges della soluzione software tramite il meccanismo dello UI Testing, possiamo dare, in quanto Team di Sviluppo, un sostanziale supporto positivo agli User Acceptance Test.


Bibliografia: User Acceptance Testing, A step-by-stepguide, Hambling, Van Goethem, Cap 2-3.