Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

Archive for the 'pattern' Category

Come adottare i test nel mondo reale

Posted by Ricibald on 25th September 2013

Perché testare (è una domanda seria…)

Testare può significare semplicemente elencare una serie di test in un Word, eseguirli secondo input specifici e riportarne l’esito in un file Excel. Significa passare nottate davanti a un Word per essere sicuri di aver scritto tutto, eseguire i test nei vari ambienti remoti con attese interminabili, riportare meticolosamente gli esiti in un foglio excel. Questo in genere ha serve solo a produrre un grafico di avanzamento al cliente, che può avere una panoramica immediata dello stato dei test.

Il motivo per cui uno sviluppatore odia i test è il fatto che non mirano a verificare realmente le funzionalità (per questo bastano anche sessioni di debug) bensì producano elaborati che servono solo per la forma, senza puntare alla sostanza. Eseguire test predeterminati non è efficace nel trovare nuovi bug, bensì fa emergere solo che i test non sono mai stati fatti, probabilmente perché abbiamo perso tempo a produrre questi file Word ed Excel che producono “fuffa” inutile. Condiamo il tutto con la ripetitività delle operazioni, la lentezza degli ambienti, la fretta e le aspettative stressanti e avremmo ciò che nessuno vorrebbe mai fare. Fare cose noiose significa farle male, quindi probabilmente sbagliarle e sentirsi dequalificati. Devo continuare con i contro…?

In realtà scrivere test dovrebbe essere un vero e proprio stile lavorativo, che deve coinvolgere la stesura dei requisiti (i test sono istanze di requisiti) e lo sviluppo in tutto il ciclo di vita. Non si tratta di forma ma di molta, molta sostanza. Scrivere i test in modo che siano semplici da scrivere, veloci da eseguire, isolati e facilmente riadattabili è una vera e propria sfida che richiede molta competenza e che di conseguenza risulta più che appassionante.

Senza noia (scrivere test è stimolante), senza stress (i test migliorano la confidenza nel codice), senza interminabili sessioni remote di troubleshooting (i test evitano regressioni) il lavoro dello sviluppatore si trasforma nell’implementare le nuove funzionalità, che costituisce la parte stimolante (e divertente) del lavoro. Devo continuare con i pro…?

Come testare…

Rispetto a un requisito dovrebbero essere previste le seguenti tipologie di test:

  1. per impedire regressioni nei layer applicativi scrivere i vari unit test secondo approccio TDD (mantra red/green/refactor). Simulare le dipendenze (es. db). Scrivere unit test significa dettagliare tramite esempi concreti il requisito spendendo circa il 50% del tempo dello sviluppo:
    • in mancanza di un input o output ben definito utilizzare il baseline test: testa, salva output in un file e al prossimo test confronta l’output con quanto salvato precedentemente nel file
    • non scrivere test specifici per un certo valore cablato, bensì rendili concettualmente veri non legandoti a un input particolare
    • non accorpare in un singolo test più clausole ma crea più test separati
    • non cambiare o rimuovere un test unitario a meno che non cambi il corrispondente requisito
    • scegli i valori di input dei test seguendo il model-driven testing: testa i valori limite e immediatamente contigui
    • al termine del giro mantra red/green/refactor verifica il code coverage che sia almeno 80% per controllare quali execution path sono stati omessi dai test
    • testare i layer applicativi come se fossero API generiche, senza legarle a un client particolare
  2. per impedire regressioni nel presentation layer (UI) scrivere coded UI test che replicano la pressione dei pulsanti o l’edit delle textbox (usando TFS o Selenium o WatiN). Simulare le dipendenze (es. layer application).
    • attenzione: questi test servono solo a testare la UI in isolamento ma non sostituiscono i test unitari dei layer applicativi
    • per semplificare la manutenzione dei test mantenere, a fronte di modifiche grafiche, gli stessi ID degli elementi grafici per non introdurre una breaking change nel test
  3. per verificare che il requisito sia interamente soddisfatto trascrivere un’istanza del requisito in un test case con i relativi step manuali dettagliati. Può essere trascritto prima o dopo aver prodotto il codice. Deve essere eseguito a implementazione completata in un ambiente completamente integrato. Un sottoinsieme sarà successivamente candidato a essere automatizzato
  4. per verificare che il requisito soddisfi caratteristiche minime creare una serie di smoke test rapidi da eseguire manualmente in sequenza
  5. per ricercare nuovi bug nel requisito condurre exploratory test. Una volta rilevata una situazione anomala è possibile trascrivere in uno script gli step precisi per consentire a chiunque di replicare il bug. Alcuni script significativi saranno successivamente convertiti in test automatizzati per garantire sempre la non regressione
    • gli exploratory test possono essere condotti seguendo diversi tour (equivalenti dei design pattern). Il tour può essere positivo (si verificano storyboard, CRUD di entità e relazioni, cambiamento di comportamento in diversi stati) oppure negativo (si verifiano variazioni insolite, invarianti dentro l’operation envelope)
  6. per verificare che un bug segnalato non si ripresenti più creare sempre un regression test che verifichi il bug. In questo modo lo stesso bug non dovrebbe più regredire
  7. per impedire bug in fase di deploy condurre dei deploy test andando sempre a generare un installer. Può essere un Installer o un IIS Deployment Package o un deployment script o un ClickOnce Deployment, non importa. Il punto è che deve esistere in quanto solo questo lo rende parte integrante del test. L’installer deve poter essere testato, come qualunque altra cosa. Molti bug rilevati sono spesso in realtà problemi in fase di deploy e determinarli con anticipo semplifica molto la gestione

Come procedere…

La sequenza descritta è quello ideale: test automatici unitari all’inizio, poi test integrati manuali e infine test automatici integrati.

Per arrivare a questo risultato un progetto ha però bisogno di tempo e lavoro per mettere in piedi una tale organizzazione. L’errore più comune in questo caso è avere una forma mentale del tipo “o tutto o niente”.

In realtà l’adozione dovrebbe essere progressiva per sostenere il cambiamento.
La progressione tipo dovrebbe essere la seguente:

  1. sfruttare l’IDE delle proprie macchine implementando i test unitari (applicativi e UI)
  2. adottare un sistema di controllo sorgente (TFS, Git, Mercurial, …)
  3. adottare un file system centralizzato per condividere documenti (TFS Portal, Dropbox, cartelle condivise, …)
  4. adottare un sistema condiviso di task & bug management (TFS, Basecamp, Zoho, Teamlab, Planbox, VersionOne, Bugzilla, Bug Genie, …)
  5. adottare un sistema di build management per abilitare il continuous integration (TFS, Jenkins, TeamCity, CruiseControl, DamageControl, …). Impostare una continuous build (“rolling”) che esegue la maggioranza dei test unitari. Impostare una build notturna che esegue test più estesi, come test di performance, di carico e di sistema
  6. adottare un sistema di gestione ed esecuzione dei test (manuali e automatici) per strutturare e automatizzare i test (Microsoft Test Manager, Telerik Test Studio, Tosca Testsuite, TestDrive, Word o Excel condiviso, …)
  7. adottare un sistema per creare automaticamente ambienti virtuali per semplificare i test in ambienti distribuiti (TFS Lab Environments, TestComplete, VmWare, VirtualBox, …)
  8. adottare un sistema per il test e deploy automatico in ambienti distribuiti (virtuali o fisici)

Già arrivare al punto 6 sarebbe un ottimo risultato :-)
Arrivare al punto 8 significa riuscire a rilasciare in produzione senza intervento umano (!).

Per sapere come si classifica il vostro modo di lavorare potete divertirvi a fare il Joel Test. Se realizzate un punteggio di 12/12 vi mando il cv :-)

Posted in pattern, test | No Comments »

Riuso o Automazione – come ottenere un riuso al 100% personalizzabile

Posted by Ricibald on 26th June 2013

Riuso con libertà assoluta di customizzazione… Connubio possibile?

Ricordiamo la metafora per i design pattern:

a * b + a * d = a * (b + d)

Significa che l’essenza dei design pattern e della buona programmazione sta nel riuso: più si riesce a centralizzare maggiore è la consistenza e l’efficienza che si ottiene.
L’efficienza è ovvia: centralizzando riusciamo a non riscrivere codice e quindi a ottimizzare il nostro lavoro. Centralizzare significa ottenere consistenza: se check di errori o log esiste nel metodo centralizzato, sarà propagata ovunque tra i chiamanti.

Esistono però ambiti dove il riuso è necessario ma molto arduo a causa delle esigenze specifiche del contesto. Un esempio tipico sono le UI (User Interface).

Immaginiamo un modello complesso da visualizzare, con 100 proprietà di tipi diversi. Nella visualizzazione o edit del modello dobbiamo visualizzare o modificare in modo consistente tutte le proprietà per conferire un look & feel autorevole. D’altra parte la visualizzazione non può nemmeno essere troppo rigida poiché si perderebbe quella flessibilità per visualizzare o modificare in modo speciale particolari proprietà. Il discorso potrebbe non essere limitato alle singole proprietà ma anche al layout stesso: potrebbe richiedere raggruppamenti speciali opzionalmente collassabili, funzionalità di drag & drop, autocompletamento, dialog ajax, popup di conferma, validazione ajax, …

Come ottenere il riuso con la giusta flessibilità?

Ci sono diverse possibilità in questo caso per ottenere il riuso con la flessibilità desiderata:

  • copia e incolla: più che di riuso si parla di “riciclo”. È una soluzione svantaggiosa? Da moltissimi punti di vista sì, ma la replicazione del codice ha anche vantaggi come la diminuzione del rischio globale (se fallisce una libreria base non fallisce tutto) e libertà assoluta di customizzazione. Ovviamente avremo bassissima consistenza ed efficienza
  • micro funzionalità fornite tramite helper: che siano classi di check, design pattern complessi, risorse (css/js), user control, html helper, aspetti, interceptor o extension method tali componenti di appoggio consentono di scrivere codice di più alto livello disinteressandoci di dettagli tecnici come log, caching, verifica errori, generazione html, transazionalità, undo metodi, … Passando opportuni parametri o implementazioni specifiche (Pattern Strategy) possiamo personalizzarne il comportamento (es. se loggare o meno) rendendole scelte comunque flessibili. Normalmente agiscono però a un basso livello: l’orchestrazione dobbiamo sempre implementarla
  • intera funzionalità autogenerata: ci troviamo nel caso più estremo, rendendo il tutto molto più consistente ma anche molto più rigido. Utilizzando metadati (da file, attributi, reflection, convenzioni, codice tramite model specializzati…) o generando codice che abilita il pattern Template Method (es. partial method) possiamo avere dei gradi di customizzazione sulle singole micro funzionalità e sull’orchestrazione stessa. Questa tecnica funziona bene e ha differenti gradi di libertà ma richiede lo sforzo di costruire un framework la cui definizione di metadati potrebbe essere non banale per ottenere il comportamento “ipotizzato” (il risultato finale potrebbe essere difficile da prevedere e non immediato). Infine, la generazione ha comunque un costo computazionale a ogni pagina generata da valutare.
  • intera funzionalità pregenerata: sembra lo stesso caso precedente, in realtà è una generazione su file che sfrutta internamente gli helper. È come se avessimo scritto noi il codice “a mano” ma in realtà è stato generato su file basandosi su metadati. Lo scopo è quello di ricondursi al secondo caso nella lista, in cui costruiamo ogni micro funzionalità tramite helper, ma risparmiando l’onere di riscrivere l’intera orchestrazione. Questo consente che l’orchestrazione sia liberamente editabile: non si otterrà la consistenza nell’orchestrazione (le modifiche in un flusso condiviso devono essere propagate a mano) ma si guadagna in efficienza. Inoltre non inficia in tempi di calcolo. Le tecniche possono essere applicazioni standalone o integrate nell’IDE come i Text Template (T4) che consentono di definire MVC Template.
  • intera funzionalità pregenerata solo su richiesta, altrimenti autogenerata: è un ibrido tra le due. Di base viene autogenerata la funzionalità magari ottimizzandone i tempi tramite un precaching e solo in caso di esigenze particolari di customizzazione si richiede esplicitamente di creare la funzionalità pregenerata su file su cui si avrà completa libertà di azione, ma partendo da uno scheletro già completamente funzionante. Un ottimo esempio in questo senso è ASP.NET Dynamic Data

Il concetto in questo post è stato generalizzato (guarda caso…) ma si può leggere “funzionalità” come “proprietà da visualizzare in input (html)” e “orchestrazione” come “layout della pagina web”: i principi si applicano quindi tranquillamente anche in un contesto web.

Non esiste solo il riuso classico…

Questo post voleva evidenziare i diversi livelli per generalizzare e abilitare il riuso: spesso ci si basa solo sulla seconda soluzione vedendo il riuso solo come la costruzione di componenti e l’utilizzo sapiente di design pattern. In realtà esiste un livello spesso non esplorato: quello della generazione automatica del codice, che elimina gran parte del lavoro pur conservando una libertà assoluta di customizzazione (come nell’ultimo caso).

Arrivare alla generazione automatica del codice ha un costo e non è detto che paghi sempre. Questi i contesti dove secondo me la generazione automatica paga:

  • UI che gestiscono varie entità in modo per lo più omogeneo
  • workflow che gestiscono varie entità in modo per lo più omogeneo
  • mapping tra entità (invece di utilizzare automapper)
  • codice collante strutturale (flusso application-domain-repository per una certa entity)
  • unit test che seguono un certo standard e certi input limite noti
  • entità e metodi derivati da sorgenti esterne (database, wsdl, xsd, api REST)
  • documentazione dei metodi per soddisfare determinati standard
  • refactoring complessi automatizzabili

Altri casi sono facilmente individuabili: ogni qualvolta ci troviamo a lavorare “come macchine” dovremmo far fare il lavoro “alle macchine” per lasciare a noi i lavori più interessanti.

Buon riuso! Anzi, buon riuso+automazione…!

Posted in pattern | No Comments »

Virtual proxy: una trappola senza uscita?

Posted by Ricibald on 20th July 2007

I virtual proxy forniscono un rappresentante di un oggetto in modo da gestirne il caricamento su richiesta, anche noto come lazy initialization.

Se immaginiamo un file word di 500 pagine, con più di 1000 immagini, il caricamento dovrebbe impegnare molto tempo e molte risorse. In realtà quel che succede è che ogni immagine non viene caricata, ma solo “dichiarata”. Nel nostro file “PROVA.DOC” ci saranno informazioni utili come width/height dell’immagine in modo che la “dichiarazione” dell’immagine consenta la corretta impaginazione.

Fin qui tutto bene, ma ora immaginiamo di aver fatto scorrere tutte le 500 pagine e di aver visto tutte le 1000 immagini. Cosa succede? Semplice: tutte le immagini sono state effettivamente caricate e abbiamo consumato molta, molta RAM (aaarghhh!!) .

Il problema (quì molto romanzato…) sembra però più serio del previsto. Citiamo il pattern Proxy:

Il design pattern Proxy fornisce un surrogato o un segnaposto di un altro oggetto per controllare l’accesso a tale oggetto.

Il client che utilizza il Proxy ne è inconsapevole: sa solamente che il comportamento atteso della classe deve essere rispettato. L’indirezione introdotta dal Proxy deve quindi essere trasparente e l’utente non può intervenire su di essa. Il clean quindi non può (e non deve) essere gestito manualmente.

Abbiamo quindi il seguente schema:

  • una classe R reale
  • un virtual proxy V che gestisce il caricamento su richiesta di R
  • un client, che utilizza V ma che in realtà è convinto di utilizzare R

Dopo tanto patire, senza ulteriori preliminari, questa è la soluzione che mi sembra più corretta (vi apparirà forse scontata ora che la leggete, ma vi garantisco che cercando e ricercando su Google nessuno ne parla…). Il nuovo contesto è il seguente:

  • una classe R reale
  • un cache proxy C, che memorizza fino a n classi reali R
  • un virtual proxy V, che se necessario richiede a C di ottenere la classe reale R
    • C restituisce la cache entry se esiste, o crea il corrispondente R memorizzando il risultato in cache
    • se nella memorizzazione del nuovo cache entry viene superato il limite n di cache:
      • viene liberata la cache eliminando l’elemento meno richiesto (least frequently used)
      • vengono notificati tutti gli V che ne facevano uso, che impostano a null la classe reale R utilizzata richiamando funzioni di liberazione della memoria come la garbage collection. La notifica si deve basare su un protocollo condiviso tra V e C, utilizzando ad esempio il design pattern Observer.
  • un client, che utilizza V ma che in realtà è convinto di utilizzare R

Un modo semplice di implementare la tecnica “least frequently used” è la strategia “move-to-front” descritta in Pattern Oriented Software Architecture: a ogni richiesta di una cache entry l’elemento viene spostato alla testa di una lista. Quando è necessario liberare memoria possono essere eliminati gli elementi alla coda della lista. Questo può essere implementato (ad es. in Java) estendendo i metodi get() e put() di una Map.

Questa (da quanto ne so) è una soluzione “casalinga”, che non si basa su tecniche o pattern noti. Perciò qualunque suggerimento/critica sarà più che apprezzata…

Posted in pattern | 2 Comments »