Continuous Integration

by Martin Fowler and Matt Foemmel
originale: http://martinfowler.com/articles/continuousIntegration.html

ottobre 2003 - traduzione di Federico Spinazzi

Debugger volant, scripta manent

Un elemento importante di ogni processo di sviluppo del software e' quello di poter ottenere una build affidabile. Viene qui presentato un processo che puo' essere molto utile. E' incentrato sul concetto di build completamente automatica e riproducibile, testing incluso che puo' essere eseguita molte volte al giorno. Questo processo permette ad ogni sviluppatore di integrare il proprio lavoro con quello degli altri in modo continuo, riducendo i problemi dell'integrazione stessa.1

Nota: ThoughtWorks ha reso open source CruiseControl (http://cruisecontrol.sourceforge.net), un software per automatizzare il processo di Continuous Integration ed offre consulenza su CruiseControl, Ant ed implementazione di Continuous Integration. Potreste trovare interessante anche l'articolo di Bill Caputo e Oren Miller's,Continuous Integration with C++.

Lo sviluppo di software e' colmo di best practices di cui si parla spesso ma che non vengono applicate quasi mai. Una delle piu' basilari e preziose e' quella di avere un processo di build e di test completamente automatico che permetta ad un team di modificare, compilare e testare un progetto piu' volte al giorno. L'idea di una build al giorno e', diciamo, il minimo: un processo completamente automatico che ne permetta piu' di una e' abbastanza facilmente implementabile.

Sebbene il termine 'Continuous Integration' sia utilizzato in riferimento alle pratiche di Extreme Programming, e' probabilmente una parte essenziale di un'attivita' di sviluppo software professionale.

Ci sono diversi elementi costitutivi di un processo automatico funzionante:

Una volta configurato un ambiente capace di suppiortare questo processo, non e' necessario un cosi' grande sforzo per mantenerlo in funzione.

Benefici della Continuous Integration

Una delle cose piu' difficili da trasmettere sulla Continuous Integration e' che comporta un fondamentale cambiamento del processo di sviluppo, un cambiamento che non e' facile capire se non lo si e' mai visto in funzione. Molte persone avvertono questo tipo di atmosfera lavorando da sole, perche', per cosi' dire, 'integrano con se stesse': lo sviluppo in team, si pensa, comporta solo problemi. In cambio di un poco di disciplina, la Continuous Integration riduce questi problemi, permettendo al gruppo di essere motlo di piu' della somma delle parti.

Uno dei benefici fondamentale e' quello di eliminare le sessioni di lavoro in cui gli sviluppatori spendono il loro tempo dando la caccia a bachi introdotti da un'altra persona il cui lavoro influisce su quello di altri, senza che nessuno stia realizzando quello che succede. Questi bachi sono difficili da trovare perche' hanno origine nell'interazione tra due 'pezzi' di lavoro; spesso, inoltre, si menifestano molto tempo dopo essere stati introdotti.

Con una Continuous Integration la maggior parte di questi bachi si manifesta il giorno stesso. Inoltre risulta visibile in quale meta' dell'interazione risieda il baco. Questo facilita la ricerca del baco e, nel caso non si riesca a trovarlo (e' sempre una questione di tempo) e' possibile evitare temporaneamente di mettere in produzione le modifiche che l'hanno introdotto (ovviamente si potrebbero desiderate piu' quelle modifiche di quanto non si sopporti il baco, ma almeno si fa una scelta motivata).

Non c'e' garanzia che tutti gli errori ed i bachi vengano intercettati. La tecnica si basa sui test e il test non dimostra l'assenza di errori: il punto e' che in questo modo il numero di errori e bachi che si identificano ripaga del costo della Continuous Integration.

Il risultato netto e' una maggiore produttivita' (anche se la C.I. ne' e solo l'elemento apice). La Continuous Integration diminuisce a tal punto il tempo speso nell'inferno dell'integrazione da far si che non sia poiu' un inferno.

Piu' spesso e' meglio

E' meglio integrare spesso che raramente: e' un'osservazione che sembra contraddire l'esperienza, ma la pratica di un integrazione continua dimostra il contrario.

Se l'integrazione viene fatta solo occasionalmente diventa un esecizio frustrante, che richiede molto tempo ed energia e non c'e' nessun incentivo a renderla piu' frequente. Spesso si sente dire: "in un progetto di queste dimensioni non e' possibile integrare giornalmente".

Ci sono progetti che lo fanno: Microsoft ha build giornaliere di progetti di milioni di linee di codice.

La ragione per cui cio' e' possibile e' che lo sforzo di integrazione e' tanto maggiore tanto piu' tempo passa tra due integrazioni diverse.

La chiave e' l'automatizzazione. La maggior parte dell'integrazione va svolta in maniera automatica: ottenere i sorgenti, compilare, eseguire i test. Alla fine resta un'indicazione precisa sull'esito: si o no. Nel secondo caso e' suffiente ottenere la precedente configurazione per avere una versione funzionante: non c'e' da fare altro.

Con un processo come questo e' possibile effettuare un numero arbitrario di build, assendo solo limitato dal tempo necessario per ognuna.

In che cosa consiste una ''Successfull Build''

E' importante decidere in che cosa consista una build completa. Puo' sembrare ovvio, ma non definire cosa sia puo' far correre il rischio di confondere un tentativo di build con una build che ha avuto successo.

Una definizione abbastanza aggressiva e' la seguente:

Molte persone considerano la sola compilazione come una build, ma e' decisamente piu' opportuno avere una test suite, che aggiunge valore alla Continuous Integration e rende il codice piu' robusto.

Single Source Point: il sorgente e' in un unico punto

Per poter integrare in modo semplice ogni sviluppatore deve poter ottenere facilmente il codice sorgente nel suo stadio attuale: non c'e' nulla di peggio che andare da ogni sviluppatore, chiedere una copia dell'ultima versione del loro codice capire dove vadano messi...

Lo standard e' semplice: ognuno deve poter collegare una macchina alla rete e con un singolo comando ottenere tutto cio' che e' necessario.

La soluzione piu' semplice e' quella di utilizzare un sistema di controllo delle revisioni come. Dal momento che esiste un sistema di tal genere open-source (CVS), il costo non e' un problema.

Affinche' questo sistema funzioni tutti i file devono essere tenuti sotto controllo delle revisioni (configuration management). Quando si dice tutto ... si includono gli script per il build, i file di configurazione, gli script con i DDL degli schema dei database: tutto cio' che e' necessario per la build su una macchina 'vergine'.

A volte si utilizzano progetti separati nel sistema di controllo delle revisioni. Cio' comporta che e' necessario ricordardi quale versione di un componente e' compatibile con quale versione di un altro componente. E' meglio, se possibile, non separare i sorgenti: dopotutto e' possibile che gli script di build possano creare i diversi componenti dallo setsso sorgente.

Build automatica e scripts

Build puo' sgnificare molte cose, dalla compilazione di qualche file (questione di un solo comando del compilatore) a script molto complessi. E' possibile avere codice generato da altro codice, prima che possa essere compilato. I test devono poter essere eseguiti automaticamente.

Un tool di build e' capace di eseguire i passi in modo incrementale, in modo che si possa gestire anche un sistema complesso, per cui la build completa durerebbe molto.

Uno script di build deve poter permettere la scelta del target.

Esempi di questo tipo di tool sono make, ant, nant.

Gli ambienti di sviluppo integrato gestiscono la build, ma sono piu' difficilmente estensibili, utilizzano spesso dei formati proprietari. Inoltre e' neessario l'ambiente di sviluppo per poter eseguire la build; questo non si adatta molto bene a situazioni in cui esiste sempre una master build eseguita in maniera automatica (ad esempio schedulata).

Self-Testing Code

Avere un programma che compila non e' sufficiente, anche se i compilatori dei linguaggi tipizzati possono identificare diversi problemi. Porre molta enfasi sulla presenza di test permette di aumentare la qualita' del software, e di supportare il cambiamente dei requisiti attraverso il refactoring.

I test sono principalmente di due tipi: test unitari e funzionali. I test unitari sono scritti dagli sviluppatori e testano tipicamente una sola od un piccolo numero di classi.

I test funzionali vengono magari scritti da un gruppo esterno e testano l'intero sistema come black box. E' possibile automatizzare entrambi (esistono infatti diversi strumenti per testare automaticamente applicazioni web, database, sistemi di messagististica, interfacce grafiche, etc...).

Come parte del processo di build e' possibile definire un insieme di tests definiti come Build Verification Tests. Tutti questi test devono poter essere esguiti con successo per considerare la build; tutti i test unitari fanno parte di questi test (questi test comunque, riguardano principalmente il processo di build, non fanno parte dell'insieme di tutti i test che probabilmente verranno definiti, la maggior parte dei quali sono test funzionali e che quindi hanno piu' a che fare con la gestione delle release del prodotto che non con la build).

Il principio base e' che quando uno sviluppatore scrive del codice deve anche scrivere i test per quel codice. Ne repository del codice sorgente vine messo anche il codice sorgente dei test. E' possibile anche adottare il metodo di Extreme Programming: non scrivere nessun codice finche' non hai per esso un test che fallisce. Percio' se vuoi aggiungere una funzionalita', prima scrivi un test che funziona in presenza di essa e poi fallo funzionare.

Se si ha a disposizione un framework per scrivere ed eseguire i test nello stesso linguaggio in cui si sta sviluppando, scrivere test e' piu' o meno la stessa cosa che scrivere codice. Per Java e' possibile utilizzare JUnit (JUnit e' il membro Java della famiglia xUnit - esistono versioni per quasi tutti i linguaggi http://www.xprogramming.com/software.htm).

Se si lavora con questo approccio, gli sviluppatori eseguono un sottoinsieme dei test per ogni compilazione, mano a mano che sviluppano. Dal momento che i test aiutano ad identificare gli errori, questo ha beneficio sui tempi di sviluppo. Inoltre, invece di eseguire il debug e' verosimile trovare gli errori nelle ultime modifiche che dovrebbero essere piccoli.

Non tutti seguono il modello test-first dell'Extreme Programming, ma il beneficio chiave proviene dal fatto di scrivere i test nello stesso momento in cui si scrive il codice. Questo velocizza la sviluppo e permette alla test suite che viene eseguita con la build di mettere in luce gli errori: effettuare debug guardando il codice cambiato e' piu' efficiente che non effettuare il debug di un programma in esecuzione.

Test non perfetti, anche se non sono esaustivi, sono meglio di test perfetti mai scritti ed eseguiti.

Una questione collegata e' quella relativa a chi deve scrivere i test. Testare il proprio codice non e' la soluzione ottimale, nonostante cio' induce un'inserimento piu' veloce dei test nel codicee questo crea un valore probabilmente maggiore di quello di avere delle persone separate che si occupano di test (almeno di questo tipo di test). E' buona norma aggiungere un test che riproduce un bug, prima di rimuovere il bug stesso.

The Master Build

Se una build automatica ha senso per il singolo sviluppatore, probabilmente il valore maggiore di essa sta nella prodzione della master build; la presenza di una master build favorisce la comunicazione e, in ogni caso, permette di identificare i problemi legati all'integrazione piu' velocemente.

CruiseControl, un tool di ContinuousIntegration per Java e .NET

Nella maggior parte dei casi vale la pena che ci sia una macchina dedicata a questo tipo di build. Su questa macchina e' in esecuzione un demone sempre in esecuzione.

Se non c'e' nessuna build in esecuzione il processo verifica il repository ad intervalli di qualche minuto: se nessuno ha effettuato un check'in dall'ultima build nulla accade, altrimenti esegue il processo di build.

Il primo passo e' quello di estrarre dal repository una copia completa del codice sorgente. Successivemente viene eseguito lo script di Ant definito per il progetto corrente.

Non importa quanto complesso possa essere il processo di build, se contiene o meno una fase di deploy, se comporta la creazione di uno schema su di un database, il caricamento di dati, l'avvio del web server, etc.

Successivamente vengono eseguiti i test della build verification suite. Se i test hanno successo viene aggiunta una nuova etichetta nel repository in corrispondenza dello stato attuale del sorgente.

Alla fine del processo viene spedito agli sviluppatori un messaggio di posta elettronica che sintetizza lo stato della build (non e' carino lasciare l'edificio dopo aver committato delle modifiche al sorgente e prima di aver ricevuto questa mail).

Cruise Control produce un log che una servlet permette di visualizzare. La pagina web creata dalla servlet mostra diverse informazioni:

Queste informazioni danno il senso di queelo che succede mano a mano che vengono effettuate aggiunte e modifiche al sorgente.

E' importante a questo punto che ogni sviluppatore sia in grado di simulare la master build il localein modo da affrontare i problemi che si verifica nella master build senza bloccarne il processo. Inoltre cio' e' permette di simularla prima di committare le proprie modifiche al repository.

E' possibile schedulare tipi di build diverse: anche se una cosidetta clean-build (build a partire da zero) impiega maggior tempo, essa garantisce sempre di essere riproducibile. In alcuni scenari e' comunque possibile alternare tra build complete ed incrementali.

Checking in: commit delle modifiche al codice sorgente

Utilizzare un processo di builld automatico significa seguire un ritmo e la parte piu' importante di esso e' quellod dato dall'integrazione regolare. E' possibile effettuare build giornaliere ma che gli sviluppatori committino meno frequentemente e quindi il sistema non funziona.

Prima di iniziare a lavorare su di un nuovo compito e' necessario allinearsi con il repository, altrimenti si rischia di lavorare su codice non aggiornato.

Una volta che si comincia a lavorare, per poter comittare non e' cosi' importante se si ha finito tutto, ma piuttosto se il codice che viene committato passi tutti i test.

La fase di commit consiste nella ri-sincronizzazione con il repository, dal momento che nel frattempo potrebbero essere occorse delle modifiche da parte di altri sviluppatori. Se qualcosa e' cambiato, build e test vanno rieseguiti.

Alcuni processi di check-in forzano una serializzazione su se stessi: c'e' un build token che puo' essere posseduto da un solo uno sviluppatore. In questo modo nessuno puo' aggiornare il repository tra una build e l'altra.

In pratica la necessita' di questa sincronizzazione si manifesta raramente e se anche si verifica il fallimento della build, di solito e' facile da correggere.

Conclusioni

Lo sviluppo di un processo di build automatico e' essenziale.

La chiave e' automatizzare tutto ed eseguire la build frequentemente, in modo da trovare in fretta gli errori. In questo modo ognuno e' preparato a modificare il codice se necessario, perche' ognuno sa che e' facile identificare e correggere gli errori.

Se si tiene conto della possibilita' offerta da motli framework di eseguire test su database, applicazioni web, interfacce grafiche, etc. si capisce come una volta che questi benefici sono dati per acquisiti, sara' difficile rinunciarvi.
____
   1 Queste note sono tratte dall'articolo di Martin Fowler http://martinfowler.com/articles/continuousIntegration.html

1