La Continuous Ingetration, in breve CI, è una pratica di sviluppo software nella quale i membri di un team integrano il proprio lavoro frequentemente, almeno una volta al giorno, con lo scopo di individuare subito eventuali problemi ed avere un software rilasciabile in tempi brevi.
L'obiettivo principale della CI è quello di evitare il cosiddetto integration hell, ossia il caso limite in cui ciascun membro del team si trova ad integrare il proprio lavoro con quello altrui solamente a sviluppo completato.
Questo processo rischia di diventare, come suggerisce il nome, un inferno: i bug ed imprevisti sono per definizione non predicibili, non sappiamo a priori quanti ce ne sono, né possiamo sapere quanto tempo ci voglia a risolverli. Avete mai sentito dire qualcuno "prevedo 10 bug in questo progetto ?", ne dubito :-)
Come possiamo pretendere, allora, di stimare un tempo di integrazione a fine progetto, quando non abbiamo nemmeno una predizione a livello locale dei problemi emersi ? Siamo finiti nel tunnel: la scadenza è sempre più vicina, lo sviluppo è terminato e non abbiamo idea di quando il software sarà correttamente funzionante, anche se tecnicamente "è finito".
Lo stress nel team aumenta con l’avvicinarsi della deadline: si rimane in ufficio fino a tardi per fixare e debuggare, con il rischio che la stanchezza ed il malumore accumulati portino inevitabilmente all’introduzione di nuovi bug per risolvere quelli precedenti.
Tutto questo non ci piace, ma la CI ci viene in aiuto, cercando di prevenire il disastro più che risolverlo. Il concetto chiave è infatti quello di spostare il momento dell'integrazione: non più alla fine del processo di sviluppo, ma inserirlo all'interno di esso. Da quanto detto, il perché di "Continuous": rendere questo processo continuo permette di individuare subito gli errori, con il vantaggio che molto probabilmente saranno poco gravi. Effettuare piccoli cambiamenti di codice ed integrarli subito significa dover correggere al più bug recenti e non interi moduli software, con il risultato di avere in qualsiasi momento un software già integrato.
Panoramica di un ambiente di CI
Gli attori fondamentali in un ambiente di Continuous Integration sono:
- Lo sviluppatore
- Un sistema di controllo di versione (SVN, git …), per avere un unico punto dove reperire il codice che stiamo sviluppando.
- Un sistema di automazione delle build (phing, ant), per eseguire automaticamente tutti i task automatizzabili al fine di ottenere il funzionamento del nostro software.
- Un sistema di testing delle build (phpunit), per eseguire test automatici sul nostro codice, che attestino oggettivamente che sia funzionante.
- Un server di Continuous Integration (jenkins, hudson), ossia un software per eseguire l'integrazione finale di tutti i cambiamenti inviati dai vari sviluppatori.
Il funzionamento generale del sistema è il seguente:
- Lo sviluppatore esegue i cambiamenti necessari al codice: implementa la feature richiesta, fino a che non la ritiene completata. Insieme ad essa, scrive i test automatici che la riguardano.
- Successivamente, esegue una build privata sulla propria workstation: attraverso il processo di build, il software viene compilato e testato. In ambito PHP il concetto di build è leggermente diverso: il software in questo caso non viene compilato, ma vengono comunque eseguiti tutti i task necessari alla sua corretta preparazione prima del rilascio; in questa fase intervengono i test automatici.
- Quando i test automatici certificano che il software è funzionante, lo sviluppatore invierà i propri cambiamenti al sistema di controllo di versione.
- A questo punto la palla passa al Continuous Integration server: periodicamente esso reperirà l'ultima versione del software dal repository ed effettuerà una nuova build automatica, con la quale testerà nuovamente il software, che sarà il prodotto dei cambiamenti inviati da tutto il team.
- Se tutto il processo è andato a buon fine, verrà generato un feedback positivo (es: tramite e-mail o un monitor ben visibile dal team) ed il software è pronto al rilascio. Viceversa il feedback sarà negativo e priorità del team sarà risolvere il bug introdotto.
Le pratiche fondamentali della CI
Adesso abbiamo una panoramica di un ambiente di CI, come possiamo notare gli elementi che lo compongono sono pochi e semplici, ma è di fondamentale importanza rispettarne le best practises di utilizzo. La Continuous Integration è innanzitutto una pratica: è molto più importante usare bene pochi strumenti, piuttosto che dotarsi di sofisticati sistemi ed utilizzarli nel modo sbagliato.
Il repository deve contenere tutto il necessario
Nel nostro sistema di controllo di versione deve essere presente tutto il necessario al funzionamento del nostro software, framework e librerie esterne compresi. Qualsiasi sviluppatore deve essere in grado di poter iniziare il proprio lavoro sul software semplicemente effettuando una checkout dal repository. Questa regola include anche eventuali database: se il nostro software ne utilizza uno, anch'esso dovrà essere presente nel sistema di versioning.
Automatizza le build e fa in modo che siano testate
Il processo di build del software deve essere automatico. Attraverso un unico comando dobbiamo essere in grado di poter far passare il nostro codice attraverso una serie di processi, che vanno da un setup iniziale all'esecuzione di test automatici. Rendere automatico il più possibile significa limitare gli errori: non è umanamente possibile testare ogni volta il software a mano e pretendere di averlo fatto sempre allo stesso modo. Oltre a ciò, non dimentichiamo che la procedura stessa di build è salvata nel repository: anch'essa è sotto controllo di versione e come ogni parte del nostro software è soggetta ad evoluzione e miglioramenti.
Committa frequentemente il tuo codice, tieni aggiornata la tua copia
È forse la regola più importante, la cui violazione fa cadere tutto il sistema: bisogna procedere per piccoli cambiamenti, ognuno dei quali va inviato al repository una volta che la propria build privata risulta funzionante. Conseguenza di questa regola è: esegui una build ad ogni cambiamento.
Prima di eseguire qualsiasi modifica, inoltre, attestati sempre di avere nella tua workstation l'ultima versione del software: più il software che andrai a modificare sarà distante dall'ultimo presente nel repository, più sarà elevata la probabilità di incorrere in problemi di integrazione.
Non inviare codice difettoso e non scaricarlo
Se la tua build privata non è funzionante, il codice difettoso non va assolutamente inviato al repository: non dimentichiamoci che uno degli scopi principali della CI è quello di avere nel repository un software sempre funzionante e rilasciabile. È perciò grave violare questa regola, anche perché potrebbe finire nelle workstation locali degli altri membri del team, col risultato di rallentare il lavoro di tutti.
Se nel repository è presente del codice difettoso, evitare di scaricarlo. In questo caso è preferibile avere una build meno aggiornata ma funzionante: se si partisse da una build rotta, non si avrebbe più la garanzia che lo sia per problemi pregressi o se i cambiamenti effettuati non abbiano introdotto altri bug.
Ripara immediatamente le build rotte
Il server di continuous integration si occupa di testare periodicamente (e più approfonditamente) il codice inviato da tutti gli sviluppatori. Nella malaugurata ipotesi che questa build fallisca, il team deve considerare come prioritaria la soluzione dei problemi emersi piuttosto che lo sviluppo di nuove funzionalità.
Non dimentichiamoci infatti che è vietato scaricare codice non funzionante, di fatto la regola descritta in precedenza attiverebbe subito tutto il team alla soluzione del problema, in quanto non potrebbe scaricare il codice sulla propria macchina per continuare il proprio lavoro.
Build veloci e feedback rapido
Il processo di build, soprattutto per quelle private, deve essere il più veloce possibile, al fine di non disincentivare lo sviluppatore dall'utilizzo della pratica. Se le build fossero lente, infatti, saremmo tentati di scrivere molto codice prima di testarlo. Per ottenere ciò, possiamo utilizzare varie strategie: dall'utilizzo di risorse hardware aggiornate, alla scrittura di script di build che facciano eseguire prima i test automatici con più probabilità di fallire.
Un'altra soluzione è quella di effettuare uno staging delle build: fare in modo che nelle build private e di integrazione ci siano tutti i test fondamentali e spostare in build meno frequenti processi come quello della code analysis.
Pro e contro
In questa introduzione abbiamo visto come la Continuous Integration sia innanzitutto una pratica e non il semplice utilizzo di strumenti, è quindi necessario accettare un'inevitabile curva di apprendimento, tanto più ripida quanto più le competenze del team sono lontane dai concetti appena illustrati.
L'utilizzo di test automatici richiede tempo, ma non dimentichiamoci che sono anche la nostra rete di salvataggio: noteremo un aumento della qualità e vivremo meglio se tutto quel che viene sviluppato è sempre accompagnato da opportuni test.
Anche l'acquisto di macchine dedicate e risorse hardware aggiornate ha un suo costo, che però va inteso come un investimento, avere un feedback immediato e condiviso porta con se un aumento della comunicazione: in qualsiasi momento ciascun membro del team conosce lo stato dei lavori e può intervenire prontamente in caso di necessita. Oltre a ciò, non dimentichiamoci che il lavoro di una persona non è minimamente comparabile con quello di un computer: perché sprecare il prezioso tempo di uno sviluppatore, quando potrebbe benissimo farlo una CPU ?