Un
esempio pratico di Continuous Integration
Nell'esempio
proposto verrà usata un'applicazione J2EE minimale
(la MokaDemo) come "cavia" del processo di CI. Si
utilizzerà JBoss come Application server, CVS come
sistema di version control, ANT per gli script e il framework
JUnit per i test. Il motore per la CI sarà Anthill.
Tutti i prodotti sono open source e liberamente scaricabili
da Internet (vedere [JBOSS],[CVS], [JUNIT],[ANT], [ANHILL]).
Figura 1: Esempio di Continuous Integration
Applicazione
J2EE di esempio
L'applicazione
J2EE (MokaDemo) è volutamente minimale e prevede, lato
presentation, una Web App sviluppata sul Framework Struts
(vedere [STRUTS]) che si basa sul pattern MVC.
L'applicazione prevede un form che richiede l'inserimento
dell'identificativo del cliente per il quale si vogliono vedere
visualizzati i dati del conto corrente.
Figura 2: L'applicazione J2EE d'esempio
(clicca sull'immagine per ingrandire)
Una
volta inserita la userid, il controller (AccountAction) invoca
l'azione di business (GetAccountCommandBean), riceve l'object
model (AccountModel) e invoca la vista successiva passandole
i dati da visualizzare (AccountView).
public
final class AccountAction extends Action {
public ActionForward execute(
.) throws Exception
{
...
String userid = (String) PropertyUtils.getSimpleProperty(form,
"userid");
...
GetAccountCommandBean cmd = new GetAccountCommandBean(userid);
cmd = (GetAccountCommandBean) cmd.execute();
account = cmd.getAccount();
AccountView av = new AccountView(account);
// create view
...
request.setAttribute( Constants.ACCOUNT_KEY,
av); // set view into request
return (mapping.findForward("showAccount"));
// Forward control to the specified success URI
}
}
Il
lato Business è incentrato sul Command Design Pattern
realizzato attraverso un EJB (CommandEJB) che funziona da
"ponte" tra le richieste di servizio (ovvero i "comandi",
rappresentati da classi che estendono la classe AbstractCommandBean)
e le classi che esaudiscono queste richieste (le quali estendono
AbstractCommandTargetBean).
Il server riceve questi comandi (richieste di servizio) generici,
li interpreta e li esegue (senza entrare nel merito sul loro
tipo) inserendo, nel comando stesso, il risultato.
Questo meccanismo infrastrutturale è reso disponibile
allo sviluppatore tramite il package it.mokabyte.j2eedemo.core.
La parte applicativa è costituita da un comando GetAccountCommandBean
(l'interfaccia del servizio) che richiede come dato in input
l'identificativo dell'utente e come ouput fornisce il modello
del conto corrente.
public
class GetAccountCommandBean extends AbstractCommandBean{
private String userid = null; // input
private AccountOM account = null; // output
public GetAccountCommandBean(String userid) {
this.userid=userid;
}
public String getTargetCommandBeanName(){
return "it.mokabyte.j2eedemo.apps.account.command.
target.GetAccountCommandTargetBean";
}
public AccountOM getAccount() {
return account;
}
public String getUserid() {
return userid;
}
public void setAccount(AccountOM accountOM) {
account = accountOM;
}
...
}
Il
reperimento dei dati del conto corrente è eseguito
dal target bean GetAccountCommandTargetBean (l'implementazione
del servizio) che viene mandato in esecuzione dal Command
Facade Bean (CommandEJB).
Figura 3: Esecuzione del servizio
(clicca sull'immagine per ingrandire)
La
parte applicativa "Account" è resa disponibile
allo sviluppatore tramite il package it.mokabyte.j2eedemo.apps.account.
Il
sistema di versionamento CVS
Per
la dimostrazione allegata a questo articolo si è scelto
il popolare sistema di versionamento Open-Source CVS. A titolo
informativo, si forniscono di seguito alcune precisazioni
sulle peculiarità di tale sistema e sulla terminologia
dei suoi comandi che si discosta leggermente da quella 'standard'
descritta in [MOKA_CI_1]. La prima caratteristica, che è
insita nel nome, è quella di essere un sistema di versionamento
'concorrente' (CVS=Concurrent Versions System). Questo significa
che a differenza di altri sistemi, CVS non prevede di default
la necessità di effettuare un lock sul file di cui
si esegue il checkout per modificarlo. Quindi in linea di
principio, può accadere benissimo che due sviluppatori
(magari situati in due angoli remoti del pianeta) eseguano
il checkout di uno stesso file nello stesso momento. Quando
avranno compiuto le loro modifiche dovranno quindi eseguire
il check-in: il primo che effettua questa operazione non avrà
nessun problema: aggiornerà il file contenuto nel repository
con la propria versione modificata. Il secondo sviluppatore
che tenta il check-in però si vedrà notificare
da CVS che nel frattempo qualcun altro ha già apportato
delle modifiche al repository e quindi evidenzierà
quali sono gli eventuali conflitti fra le modifiche dei due
sviluppatori. Dovrà essere cura di questo secondo sviluppatore,
meglio se di concerto con il primo che ha modificato il file,
apportare le proprie modifiche in modo da non annullare quelle
dell'altro.
Per quanto riguarda la terminologia, si noterà che
nel mondo CVS l'operazione di check-in viene generalmente
chiamata commit, mentre al termine checkout viene associata
in CVS solo l'operazione con cui si popola per la prima volta
un workspace locale con un file tratto dal repository, mentre
i successivi aggiornamenti dal repository all'area locale
vengono detti update.
Gli
script ANT
Per
l'esempio proposto in questo articolo si è scelto di
usare Apache Ant per la creazione degli script di automazione
del build. Apache Ant è un tool basato su Java e XML.
Il principale vantaggio dell'uso combinato di queste due tecnologie
è l'assoluta indipendenza dalla piattaforma; l'unico
requisito è quello di avere una JVM.
Ogni buildfile di ANT è organizzato in un progetto
caratterizzato da un nome , da un target di default e da
una directory base.
Ogni build di ANT definisce uno o piu' target; un target a
sua volta è un insieme di task che vengono processati
in sequenza.
E' possibile passare, tramite la linea di comando, il target
da eseguire; in assenza di questo viene eseguito quello di
default.
java
-cp %CLASSPATH% org.apache.tools.ant.Main -buildfile .\ant\build.xml
make-all
E'
possibile definire delle relazioni di dipendenza tra i target
ad esempio mediante la keyword depends che definisce l'ordine
con cui devono essere eseguiti i target. Ciò consente
di costruire delle pipeline (catene di montaggio) di task
in cui un task costruisce un prodotto parziale che viene poi
usato come punto di partenza da un task successivo e così
via.
I task rappresentano l'unità atomica di esecuzione
di ANT e sono implementati attraverso classi Java. Sono divisi
in famiglie (di compilazione, di deployment, ecc
) e
possono riferire proprietà o classpath definiti da
altri task.
All'interno del processo di sviluppo, ANT può essere
utilizzato per compilare il codice, creare EJB, farne il deploy/undeploy,
lanciare test, ecc
il tutto in maniera completamente
automatica e svincolata da qualsiasi IDE.
Di seguito si riporta come esempio il file build.xml che permette
la compilazione dell'intera applicazione J2EE, dove il target
make-all funziona da "Master Build" andando ad invocare
il target make-all sui sottoprogetti Apps, Core e Web per
poi provvedere al deploy dei vari archivi ottenuti.
<target
name="make-all" depends="deploy">
</target>
<target
name="deploy" depends="make_core,make_apps,make_webapp">
<copy file="${bin.dir}/mokaCoreCommon.jar" toDir="${jboss-deploy.dir}"/>
<copy file="${bin.dir}/mokaCoreClient.jar" toDir="${jboss-deploy.dir}"/>
<copy file="${bin.dir}/mokaModules_client.jar"
toDir="${jboss-deploy.dir}"/>
. . . . .
</target>
<target
name="make_core" depends="init">
<ant antfile="${basedir}/MokaCore/ant/build.xml"
dir="${basedir}/MokaCore" target="make-all"
/>
</target>
<target name="make_apps" depends="init,make_core">
<ant antfile="${basedir}/MokaApps/ant/build.xml"
dir="${basedir}/MokaApps" target="make-all"
/>
</target>
<target
name="make_webapp" depends="init,make_apps">
<ant antfile="${basedir}/MokaAppsWeb/ant/build.xml"
dir="${basedir}/MokaAppsWeb" target="make-all"
/>
</target>
I
make-all dei sotto progetti Apps, Core e Web hanno all'interno
del loro build.xml il target make-all che provvede a compilare
e a creare gli opportuni archivi.
<target
name="make-all" depends="makejar_core">
</target>
<target
name="compile" depends="init">
<javac srcdir="${src.dir}" destdir="${classes.dir}"
debug="yes" >
<classpath>
<fileset dir="${jboss.dist.dir}/server/default/lib">
<include name="*.jar"/>
</fileset>
<fileset dir="${junit.dist.dir}">
<include name="*.jar"/>
</fileset>
. . . .
</classpath>
</javac>
</target>
<target
name="makejar_core_client" depends="compile">
<jar jarfile="${bin.dir}/mokaCoreClient.jar"
manifest="${deploy.dir}/ejb/Manifest.mf">
<fileset dir="${classes.dir}" >
<include name= "it/mokabyte/j2eedemo/core/command/*"/>
<include name= "it/mokabyte/j2eedemo/core/command/exception/**"/>
<include name= "it/mokabyte/j2eedemo/core/util/**"/>
. . . . . .
</fileset>
</jar>
</target>
I
test con JUnit
Un
processo di CI deve immancabilmente prevedere la presenza
di una suite di test di non regressione per verificare la
correttezza del codice da costruire.
JUnit
è un framework nato per semplificare lo sviluppo di
test di unità. E' integrabile in ANT mediante il task
(opzionale) junit.
Nell'esempio che segue si riporta un estratto della classe
di test che verifica il funzionamento del comando di business
GetAccountCommandBean:
public
class TestApps extends TestCase {
public static Test suite() {
TestSuite suite = new TestSuite("Test
Apps");
suite.addTest(new TestApps("testAccountDao"));
suite.addTest(new TestApps("testAccountCommandBean"));
...
return suite;
}
public void testAccountCommandBean() {
try {
GetAccountCommandBean
cmd = new GetAccountCommandBean(USERID);
cmd = (GetAccountCommandBean)
cmd.execute();
AccountOM result = cmd.getAccount();
assertNotNull(result);
assertEquals("CHECK
CONFRONTO SALDO", result.getBalance(), BALANCE, 0);
} catch (Exception e) {
fail(e.getMessage());
}
}
}
Il
lancio dei test viene schedulato come ultimo passo della CI
ed è invocato tramite ANT che è anche in grado
di generare un report HTML della Test Suite tramite l'utilizzo
del tag <junitreport>
<target
name="run_testclient" depends="compile">
<junit fork="yes">
<jvmarg value="-Djava.naming.factory.initial=org.jnp.interfaces.NamingContextFactory"/>
<jvmarg value="-Djava.naming.provider.url=jnp://${jboss.host}:1099"/>
<jvmarg value="-DMDConfigurationRoot=${basedir}/../config"/>
<formatter
type="plain"/>
<test
name="it.mokabyte.j2eedemo.test.core.TestCore" haltonfailure="no"
outfile="resultCore">
<formatter type="xml"/>
</test>
<test name="it.mokabyte.j2eedemo.test.apps.TestApps"
haltonfailure="no" outfile="resultApps">
<formatter type="xml"/>
</test>
<classpath>
<dirset dir="${classes.dir}"/>
<fileset dir="${bin.dir}" includes="*.jar"/>
<pathelement location="${jboss.dist.dir}/client/jboss-j2ee.jar"/>
<pathelement location="${jboss.dist.dir}/client/jbossall-client.jar"/>
. . . . . . . . . .
<pathelement location="${xerces.dist.dir}/xercesImpl.jar"/>
</classpath>
</junit>
<junitreport todir=".">
<fileset dir=".">
<include name="result*.xml"/>
</fileset>
<report format="frames" todir="${dist.tests.dir}"/>
</junitreport>
</target>
Il tool di Continuous Integration Anthill
Fra
i tool di Continuous Integration disponibili, si è
scelto di utilizzare Anthill, che risulta molto facile da
installare e configurare. Altri prodotti simili sono CruiseControl
realizzato dalla ThoughtWorks e Gump che fa parte del progetto
jakarta-apache. Anthill è disponibile sia come progetto
OpenSource che si può scaricare liberamente dal sito
[ANT_HILL] sia come progetto commerciale, che fornisce diverse
funzionalità a completamento di quelle presenti nella
versione Open.
Il
cuore di Anthill è costituito da una WebApp, che viene
fornita tramite un archivio war che va deployato così
com'è in un servlet container (non fornito con Anthill).
Noi abbiamo utilizzato Tomcat (versione 4.0.19). Una volta
fatto il deploy, dovreste poter raggiungere l'applicazione
anthill tramite un url del tipo http://localhost:8080/anthill
Figura 4: Schermata iniziale di Anthill
Figura 5: Impostazioni Anthill del progetto di esempio
A
questo punto potete creare un nuovo progetto in AntHill agendo
sull'apposito link "Create New Project". Si dovranno
inserire informazioni generali, quali il nome del progetto
in Anthill, il path, relativo alla root del progetto, dove
si trova lo script di ant che avvia il build, gli indirizzi
e-mail degli utenti che dovranno essere notificati dopo la
conclusione dei build schedulati ecc. Inoltre sarà
necessario configurare le informazioni specifiche a CVS, come
il nome del 'module' CVS che contiene il Progetto in CVS;
la CVSROOT da utilizzare per collegarsi al vostro server CVS
e l'utente che figurerà come l'autore delle operazioni
fatte da Tomcat/Anthill sul Repository.
Figura 6: Impostazioni di Anthill per CVS
(clicca sull'immagine per ingrandire)
Un'ultima
impostazione da effettuare riguarda il path (sempre relativo
alla root del progetto) di un file di testo che è necessario
mettere sotto version control e che è preposto a contenere
il numero del build corrente. Attraverso il seguente form
si imposta il nome scelto per tale file. Il suo contenuto
iniziale dovrà essere una stringa del tipo "1.0.0".
Ad ogni successivo build, Anthill provvederà ad aggiornare
la terza cifra con un numero progressivo. Le prime due cifre
viceversa, dovranno essere cambiate manualmente, per indicare
cambi di versione 'Major' e 'Minor'.
Figura 7: Impostazioni del file che contiene il build
number
Una
volta terminate le necessarie impostazioni, è possibile
defiire una politica di scheduling che esegua il build ad
intervalli specificati. E' d'altra parte possibile in qualunque
momento invocare un build non schedulato.
Anthill
non richiede di effettuare alcuna modifica agli script di
build; tuttavia è utile tenere presente che quando
Anthill invoca un certo build.xml, passa implicitamente una
variabile chiamata "deployDir", valorizzata in
modo da puntare ad una directory 'pubblicata' dalla WebApp
di Anthill e specifica al progetto in questione. E' quindi
molto utile sfruttare questa variabile all'interno dei propri
build file, collocando in tale path gli eventuali 'prodotti'
della compilazione, quali ad esempio:
- Esiti
dei test (ottenuti con il task <junitreport>)
- Javadoc
- Binari
prodotti dalla compilazione
Per
far sì che gli script di build funzionino anche al
di fuori del sistema Anthill, si può impostare la variabile
"deployDir" all'interno degli script in modo che
puntino ad una locazione per esempio all'interno dell'alberatura
del progetto. Così, quando viene lanciato lo script
di build autonomamente, i risultati del build verranno collocati
in tale directory. Viceversa, qualora lo script sia invocato
da Anthill, il valore specificato all'interno dello script
verrà sovrascritto da quello passato da Anthill.
La directory di pubblicazione dei risultati di un build può
essere acceduta dal link presente nella schermata iniziale
che si chiama come il progetto (nel nostro esempio moka_demo).
Nel nostro caso, da tale link si potrà accedere alle
pagine contenenti gli esiti dei test e pure alle pagine con
i log delle compilazioni eseguite, che vengono prodotte automaticamente
da Anthill nella directory 'buildLogs'.
Figura 8: Report dei test
(clicca sull'immagine per ingrandire)
Conclusioni
In
questo articolo si è presentato un esempio pratico
di Continuos Integration esemplificando i concetti introdotti
in [MOKA_CI_1].
Bibliografia
[MOKA_CI_1]
S. Rossini, A.D'Angeli: Continuous Integration: la teoria
- Mokabyte N.87 Luglio/Agosto 2004
[MFCI]Continuous Integration: Martin Fowler
http://martinfowler.com/articles/continuousIntegration.html
[JBOSS] www.jboss.org
[CVS] http://www.loria.fr/~molli/cvs/doc/cvs_toc.html
[STRUTS] http://struts.apache.org/
[ANT] http://ant.apache.org/index.html
[ANTHILL] http://www.urbancode.com/projects/anthill/default.jsp
[xUNIT] http://www.xprogramming.com/software.htm
[JUNIT] http://www.junit.org/index.htm
[TWCC] http://cruisecontrol.sourceforge.net
|