SeamFramework.orgCommunity Documentation
La maggior parte delle applicazioni Seam ha bisogno di almeno due tipi di test automatici: test di unità per testare un particolare componente Seam in isolamento, e test d'integrazione per provare tutti i layer java dell'applicazione (cioè tutto, tranne le pagine di vista).
Entrambi i tipi di test sono facili da scrivere.
Tutti i componenti Seam sono POJO. Questo è un buon punto per partire se si vogliono eseguire dei facili test di unità. E poiché Seam enfatizza l'uso della bijection per le interazioni tra componenti e l'accesso ad oggetti contestuali, è molto facile testare un componente Seam fuori dal suo normale ambiente di runtime.
Si consideri il seguente componente Seam che crea una dichiarazione di account per un cliente:
@Stateless
@Scope(EVENT)
@Name("statementOfAccount")
public class StatementOfAccount {
@In(create=true) EntityManager entityManager
private double statementTotal;
@In
private Customer customer;
@Create
public void create() {
List<Invoice
> invoices = entityManager
.createQuery("select invoice from Invoice invoice where invoice.customer = :customer")
.setParameter("customer", customer)
.getResultList();
statementTotal = calculateTotal(invoices);
}
public double calculateTotal(List<Invoice
> invoices) {
double total = 0.0;
for (Invoice invoice: invoices)
{
double += invoice.getTotal();
}
return total;
}
// getter and setter for statementTotal
}
Si può scrivere un test d'unità per il metodo calculateTotal (che testa la business logic del componente) come segue:
public class StatementOfAccountTest {
@Test
public testCalculateTotal {
List<Invoice
> invoices = generateTestInvoices(); // A test data generator
double statementTotal = new StatementOfAccount().calculateTotal(invoices);
assert statementTotal = 123.45;
}
}
Si vede che non si sta testando il recupero dei dati e la persistenza dei dati da/a database; e neppure si testa alcuna funzionalità fornita da Seam. Si sta solamente testanto la logica del POJO. I componenti Seam solitamente non dipendono direttamente dall'infrastruttura del container, e quindi la maggior parte dei test d'unità sono facili come quello mostrato!
Comunque se si vuole testare l'intera applicazione, si consiglia di continuare nella lettura.
Il test d'integrazione è leggermente più difficile. In questo caso non si può eliminare l'infrastruttura del container che invece fa parte di ciò che va testato! Allo stesso tempo non si vuole essere forzati ad eseguire un deploy dell'applicazione in un application server per fare girare i test. Occorre essere in grado di riprodurre l'infrastruttura essenziale del container dentro l'ambiente di test per poter provare l'intera applicazione senza nuocere troppo alle performance.
L'approccio adottato da Seam è quello di consentire di scrivere test che possano provare i componenti mentre girano dentro un ambiente di container ridotto (Seam assieme al container JBoss Embedded; vedere Sezione 30.6.1, «Installare JBoss Embedded» per i dettagli di configurazione)
public class RegisterTest extends SeamTest
{
@Test
public void testRegisterComponent() throws Exception
{
new ComponentTest() {
protected void testComponents() throws Exception
{
setValue("#{user.username}", "1ovthafew");
setValue("#{user.name}", "Gavin King");
setValue("#{user.password}", "secret");
assert invokeMethod("#{register.register}").equals("success");
assert getValue("#{user.username}").equals("1ovthafew");
assert getValue("#{user.name}").equals("Gavin King");
assert getValue("#{user.password}").equals("secret");
}
}.run();
}
...
}
Occasionalmente occorre essere in grado di sostituire l'implementazione di alcuni componenti Seam che dipendono da risorse non disponibili in ambiente di test. Per esempio, si supponga di avere dei componenti Seam che sono una façade di un qualche sistema di elaborazione pagamenti:
@Name("paymentProcessor")
public class PaymentProcessor {
public boolean processPayment(Payment payment) { .... }
}
Per i test di integrazione è possibile creare un mock di un componente come segue:
@Name("paymentProcessor")
@Install(precedence=MOCK)
public class MockPaymentProcessor extends PaymentProcessor {
public boolean processPayment(Payment payment) {
return true;
}
}
Poiché la precedenza MOCK
è più elevata della precedenza di default dei componenti di applicazione, Seam installerà l'implementazione mock quando questa è nel classpath. Con un deploy in produzione, l'implementazione mock è assente, e quindi il componente reale verrà installato.
Un problema ancora più difficile è quello di emulare le interazioni utente. Un terzo problema si verifica quando vengono messe le asserzioni. Alcuni framewrok di test consentono di testare un'intera applicazione riproducendo le interazioni utente con il browser. Questi framework hanno un loro spazio d'uso, ma non sono adatti per l'uso in fase di sviluppo.
SeamTest
consente di scrivere test sotto forma di script (scripted), in un ambiente JSF simulato. Il ruolo di un test scripted è quello di riprodurre l'interazione tra la vista ed i componenti Seam. In altre parole si pretende di essere l'implementazione JSF!
Questo approccio testa ogni cosa tranne la vista.
Si consideri una vista JSP per il componente testato come unità visto sopra:
<html>
<head>
<title
>Register New User</title>
</head>
<body>
<f:view>
<h:form>
<table border="0">
<tr>
<td
>Username</td>
<td
><h:inputText value="#{user.username}"/></td>
</tr>
<tr>
<td
>Real Name</td>
<td
><h:inputText value="#{user.name}"/></td>
</tr>
<tr>
<td
>Password</td>
<td
><h:inputSecret value="#{user.password}"/></td>
</tr>
</table>
<h:messages/>
<h:commandButton type="submit" value="Register" action="#{register.register}"/>
</h:form>
</f:view>
</body>
</html
>
Si vuole testare la funzionalità di registrazione dell'applicazione (la cosa che succede quando l'utente clicca il pulsante Registra). Si riprodurrà il ciclo di vita della richiesta JSF in un test TestNG automatizzato:
public class RegisterTest extends SeamTest
{
@Test
public void testRegister() throws Exception
{
new FacesRequest() {
@Override
protected void processValidations() throws Exception
{
validateValue("#{user.username}", "1ovthafew");
validateValue("#{user.name}", "Gavin King");
validateValue("#{user.password}", "secret");
assert !isValidationFailure();
}
@Override
protected void updateModelValues() throws Exception
{
setValue("#{user.username}", "1ovthafew");
setValue("#{user.name}", "Gavin King");
setValue("#{user.password}", "secret");
}
@Override
protected void invokeApplication()
{
assert invokeMethod("#{register.register}").equals("success");
}
@Override
protected void renderResponse()
{
assert getValue("#{user.username}").equals("1ovthafew");
assert getValue("#{user.name}").equals("Gavin King");
assert getValue("#{user.password}").equals("secret");
}
}.run();
}
...
}
Si noti che si è esteso SeamTest
, il quale fornisce un ambiente Seam ai componenti, e si è scritto uno script di test come classe anonima che estende SeamTest.FacesRequest
, la quale fornisce un ciclo di vita emulato della richiesta JSF. (C'è anche SeamTest.NonFacesRequest
per testare le richieste GET). Si è scritto codice in metodi che vengono chiamati in varie fasi JSF, per emulare le chiamate che JSF farebbe ai componenti. Infine sono state scritte le asserzioni.
Nelle applicazioni d'esempio di Seam ci sono molti test d'integrazione che mostrano casi ancora più complicati. Ci sono istruzioni per eseguire questi test usando Ant o usando il plugin TestNG per Eclipse:
Se è stato usato seam-gen per creare il progetto si è già pronti per scrivere test. Altrimenti occorre configurare l'ambiente di test all'interno del proprio tool di build (es. ant, maven, eclipse).
Prima si guardi alle dipendenze necessarie:
Tabella 37.1.
Group Id | Artifact Id | Locazione in Seam |
---|---|---|
org.jboss.seam.embedded
|
hibernate-all
|
lib/test/hibernate-all.jar
|
org.jboss.seam.embedded
|
jboss-embedded-all
|
lib/test/jboss-embedded-all.jar
|
org.jboss.seam.embedded
|
thirdparty-all
|
lib/test/thirdparty-all.jar
|
org.jboss.seam.embedded
|
jboss-embedded-api
|
lib/jboss-embedded-api.jar
|
org.jboss.seam
|
jboss-seam
|
lib/jboss-seam.jar
|
org.jboss.el
|
jboss-el
|
lib/jboss-el.jar
|
javax.faces
|
jsf-api
|
lib/jsf-api.jar
|
javax.el
|
el-api
|
lib/el-api.jar
|
javax.activation
|
javax.activation
|
lib/activation.jar
|
E' molto importante non inserire nel classpath le dipendenze di JBoss AS a compile time da lib/
(es. jboss-system.jar
), altrimenti questo causerà il non avvio di JBoss Embedded. Quindi si aggiungano solo le dipendenze necessarie per partire (es. Drools, jBPM).
Occorre includere nel classpath la directory bootstrap/
che contiene la configurazione per JBoss Embedded.
E sicuramente occorre mettere nel classpath il progetto ed i test così come i jar del framework di test. Non si dimentichi di mettere nel classpath anche tutti i file di configurazione corretti per JPA e Seam. Seam chiede a JBoss Embedded di deployare le risorse (jar o directory) che hanno seam.properties
nella root. Quindi se non si assembla una struttura di directory che assomiglia ad un archivio deployabile contenente il progetto, occorre mettere seam.properties
in ciascuna risorsa.
Di default un progetto generato userà per i test java:/DefaultDS
(un datasource predefinito HSQL in JBoss Embedded). Se si vuole usare un altro datasource, si metta foo-ds.xml
nella directory bootstrap/deploy
.
Seam include il supporto TestNG, ma è possibile usare un qualsivoglia altro framework di test quale JUnit.
Occorre fornire un'implementazione di AbstractSeamTest
che faccia le seguenti cose:
Chiami super.begin()
prima di ciascun test.
Chiami super.end()
dopo ciascun metodo di test.
Chiami super.setupClass()
per configurare l'ambiente di test d'integrazione. Questo deve essere chiamato prima dei metodi di test.
Chiami super.cleanupClass()
per pulire l'ambiente di test d'integrazione.
Chiami super.startSeam()
per avviare Seam all'avvio dei test.
Chiami super.stopSeam()
per terminare in modo corretto Seam alla fine dei test.
If you need to insert or clean data in your database before each test you can use Seam's integration with DBUnit. To do this, extend DBUnitSeamTest
rather than SeamTest.
Devi fornire un dataset per DBUnit.
DBUnitSeamTest
assumes the flat format is used, so make sure that your dataset is in this format. <dataset>
<ARTIST
id="1"
dtype="Band"
name="Pink Floyd" />
<DISC
id="1"
name="Dark Side of the Moon"
artist_id="1" />
</dataset
>
e comunicarlo a Seam facendo l'override di prepareDBUnitOperations()
:"
protected void prepareDBUnitOperations() {
beforeTestOperations.add(
new DataSetOperation("my/datasets/BaseData.xml")
);
}
DataSetOperation
è impostato di default a DatabaseOperation.CLEAN_INSERT
se non viene specificata qualche altra operazione come argomento del costruttore. L'esempio di cui sopra pulisce tutte le tabelle definite in BaseData.xml
e quindi inserisce tutte le righe dichiarate in BaseData.xml
prima che venga invocato ogni metodo @Test
.
Se viene richiesta un'ulteriore pulizia prima dell'esecuzione di un metodo di test, si aggiungano operazioni alla lista afterTestOperations
.
Occorre informare DBUnit del datasource usato impostando il parametro TestNG chiamato datasourceJndiName
:
<parameter name="datasourceJndiName" value="java:/seamdiscsDatasource"/>
DBUnitSeamTest supporta MySQL e HSQL- occorre dire quale database viene usato:
<parameter name="database" value="HSQL" />
Questo consente anche di inserire dati binari nel set dei dati di test (N.B. non è stato testato sotto Windows). Occorre dire dove si trovano queste risorse:
<parameter name="binaryDir" value="images/" />
Si devono specificare questi tre parametri in testng.xml
.
Per usare DBUnitSeamTest con altri database occorre implementare alcuni metodi. Si legga javadoc di AbstractDBUnitSeamTest
per saperne di più.
E' facilissimo eseguire il test d'integrazione con Seam Mail:
public class MailTest extends SeamTest {
@Test
public void testSimpleMessage() throws Exception {
new FacesRequest() {
@Override
protected void updateModelValues() throws Exception {
setValue("#{person.firstname}", "Pete");
setValue("#{person.lastname}", "Muir");
setValue("#{person.address}", "test@example.com");
}
@Override
protected void invokeApplication() throws Exception {
MimeMessage renderedMessage = getRenderedMailMessage("/simple.xhtml");
assert renderedMessage.getAllRecipients().length == 1;
InternetAddress to = (InternetAddress) renderedMessage.getAllRecipients()[0];
assert to.getAddress().equals("test@example.com");
}
}.run();
}
}
Viene creata una nuova FacesRequest
come normale. Dentro la sezione invokeApplication mostriamo il messaggio usando getRenderedMailMessage(viewId);
, passando il viewId del messaggio da generare. Il metodo restituisce il messaggio sul quale è possibile fare i test. Si può anche usare ogni altro metodo standard del ciclo di vita JSF.
Non c'è alcun supporto per il rendering dei componenti JSF standard, così non è possibile testare facilmente il corpo dei messaggi email.