The Hitchhiker's
Guide to
testable code
semplici regole per scrivere codice semplice da testare
Davide Cerbo - [email protected] - JUG Roma
Nicola Raglia - [email protected] - JUG Roma
Non parleremo di...
– XP Programming
– Test-Driven Development
– Agile
– Scrum
– etc etc
...ma parleremo di...
come scrivere codice
TESTABILE
perchè l'unico modo per applicare le metodologie dette in
precedenza è scrivere i
TEST UNITARI
e l'unico modo per scriverli è produrre codice
TESTABILE
Definizioni assortite
Test: processo atto ad individuare carenze funzionali e non
funzionali durante la fase di sviluppo del software.
Test Unitario: è un test atto a verificare una componente
elementare del software possibilmente in termini di
isolamento dalle dipendenze
Refactoring: è il processo che prevede una ristrutturazione
del codice modificando il meno possibile le interfacce
Ancora definizioni assortite
Design Pattern: soluzione progettuale generale a un
problema ricorrente
Mock Object: oggetti destinati a simulare il comportamento
di oggetti reali.
Durante il test con i mock object abbiamo:
o creazione mock
o definizione del comportamento del mock object
o esecuzione del test
o verifica del comportamento del mock object
Esempio di codice brutto
Iniziamo dal costruttore
public RubricaImpl(Properties properties, ApplicationContext applicationContext) {
this.user = applicationContext.getAuthenticationContext().getUser();
this.url = properties.getProperty("url");
this.userName = properties.getProperty("userName");
this.password = properties.getProperty("password");
try {
this.connection = DriverManager.getConnection(url, userName, password);
} catch (SQLException e) {
//gestione eccezione
}
this.database = new DatabaseImpl(connection);
}
Il nostro primo (non) Unit Test
public void testConstructor() throws Exception {
Properties properties = new Properties();
properties.load(new FileInputStream("database.properties"));
ApplicationContext applicationContext = ApplicationContext.getContext();
Rubrica rubrica = new RubricaImpl(properties, applicationContext);
}
con i Mock Objects:
public void testConstructor() throws Exception {
Properties properties = new Properties();
properties.setProperty("user", "dbuser");
properties.setProperty("password","dbpassword");
properties.setProperty("url", "jdbc:db:///test");
ApplicationContext applicationContext = createMock(ApplicationContext.class);
AuthenticationContext authContext = createMock(AuthenticationContext.class);
expect(applicationContext.getAuthenticationContext()).andReturn(authContext);
expect(authContext.getUser()).andReturn(createMock(User.class));
replay(authContext, applicationContext);
Rubrica rubrica = new RubricaImpl(properties, applicationContext);
verify(authContext, applicationContext);
}
Rispettiamo la legge
public RubricaImpl(String url, String userName, String password, User user) {
this.user = user;
this.url = url;
this.userName = userName;
this.password = password;
Connection connection=DriverManager.getConnection(url,userName,password
this.database = new DatabaseImpl(connection);
}
•
•
•
•
Per rispettare la legge di Demeter un oggetto può solo invocare i metodi:
propri
dei suoi parametri
di ogni oggetto che crea
delle sue variabili
Mai dire CONTEXT
public RubricaImpl(Properties properties, ApplicationContext applicationContext) {
this.user = applicationContext.getUser();
this.url = properties.getProperty("url");
.......
.......
}
public RubricaImpl(String url, String userName, String password, User user) {
this.user = user;
this.url = url;
.......
.......
}
applicationContext e properties sono oggetti
di contesto
quindi difficilmente testabili unitariamente e richiedono
fatica aggiuntiva nel test con i mock object.
Vietato affaticare
public RubricaImpl(String url, String userName, String password, User user) {
.....
this.userName = userName;
Connection connection = DriverManager.getConnection(url,userName,password);
this.database = new DatabaseImpl(connection);
}
public RubricaImpl(String url, String userName, String password, User user) {
.....
this.userName = userName;
this.database = DatabaseManager.getDatabase(url,userName,password);
}
Questa è una soluzione ma non
va bene perché si usa un
metodo statico
Solo l'indispensabile
public RubricaImpl(String url, String userName, String password, User user) {
this.userName =userName;
this.database = DatabaseManager.getDatabase(url,userName,password);
}
2 SOLUZIONI DA EVITARE!!!
public RubricaImpl(User user) {
this.user = user;
this.database = DatabaseSingleton.getInstance();
}
Ecco fatta un po' di pulizia!
Non era giusto far conoscere alla Rubrica le informazioni per
accedere al database!
Ma è spuntato un singleton e questo è male!
Dependency Injection
public RubricaImpl(Database database, User user) {
this.user = user;
this.database = database;
}
Il costruttore è stato alleggerito da
responsabilità non proprie.
Ma ora come lo usiamo?
Pattern Abstract Factory
public class RubricaFactoryImpl implements RubricaFactory {
private final DatabaseFactory databaseFactory;
public RubricaFactoryImpl(DatabaseFactory databaseFactory) {
this.databaseFactory = databaseFactory;
}
public Rubrica getRubrica(User user) {
return new RubricaImpl(databaseFactory.getDatabase(), user);
}
}
La responsabilità di creare oggetti sarà sempre data
ad una Factory o ad altri pattern creazionali.
Passiamo al Database
public class DatabaseFactoryImpl implements DataBaseFactory {
private final Properties properties;
public DatabaseFactoryImpl(Properties properties) {
this.properties = properties;
}
public Database getDatabase(){
String url = properties.getProperty("url");
String userName = properties.getProperty("userName");
String password = properties.getProperty("password");
Connection connection = null;
try {
connection = DriverManager.getConnection(url, userName, password);
} catch (SQLException e) { //gestione eccezione }
return new DatabaseImpl(connection);
}
}
DatabaseFactoryImpl non è testabile,
andrebbe fatto ulteriore refactoring, ma il
tempo è poco :(
Il Test (quasi) finale (1/2)
public void testConstructor() throws Exception {
Database database = createMock(Database.class);
User user = createMock(User.class);
replay(database, user);
Rubrica rubrica = new RubricaImpl(database, user);
verify(database, user);
}
Non c'è bisogno di descrivere comportamento per gli
oggetti mock perchè il costruttore non fa niente altro
che costruire l'oggetto.
Ma le factory appena create?
Il Test (quasi) finale (2/2)
public void testFactory() throws Exception {
DatabaseFactory databaseFactory = createMock(DatabaseFactory.class);
Database database = createMock(Database.class);
User user = createMock(User.class);
expect(databaseFactory.getDatabase()).andReturn(database);
replay(databaseFactory, user, database);
RubricaFactory rubricaFactory = new RubricaFactoryImpl(databaseFactory);
Rubrica rubrica = rubricaFactory.getRubrica(user);
verify(databaseFactory, user, database);
assertNotNull(rubrica);
}
Gli obbiettivi raggiunti
Single responsability
 Assegnare la giusta responsabilità
 Utilizzare la Dependency Injection
 Dividere il fare dal creare
 Evitare stati globali
 Design by Interface

Andiamo avanti...
public void publish(){
Context context = new InitialContext();
Object reference = context.lookup("PublisherService");
PublisherEjb home = (PublishEjb)PortableRemoteObject.narrow(reference,PublishEjb.class);
PublisherService publisher = home.create();
publisher.publish(this);
}
Testiamolo...
Totalmente non testabile in termini
unitari!!!
Via il Sevice Locator
public RubricaImpl(Database database, User user, PublisherService publisher) {
this.user = user;
this.database = database;
this.publisher = publisher;
}
public void publish(){
this.publisher.publish(this);
}
Iniettiamo una classe che abbia la responsabilità di
pubblicare. Nel nostro caso lo farà tramite EJB, ma sarà
semplice sostituire la tecnologia.
Ancora non è finita...
public RubricaImpl(Database database, User user) {
this.user = user;
this.database = database;
}
public void publishWith(PublisherService publisher){
publisher.publish(this);
}
Passare l'oggetto PublisherService al costruttore è errato
perché non è necessario al normale ciclo di vita della
Rubrica, ma serve solo nel caso di una richiesta di
pubblicazione
Problema solo spostato
Abbiamo solo spostato il problema, infatti
l'implementazione PublisherServiceEJB sarà
intestabile unitariamente...
...ma fortunatamente la nuova specifica EJB
3.0 ci viene in aiuto eliminando il
ServiceLocator
Ma non è lo scopo di questo talk spiegare
come :D
Il Test finale
public void testPublish() throws Exception {
Database database = createMock(Database.class);
User user = createMock(User.class);
replay(database, user);
Rubrica rubrica = new RubricaImpl(database, user);
verify(database, user);
PublisherService publisherService = createMock(PublisherService.class);
publisherService.publish(rubrica);
replay(publisherService, user);
rubrica.publishWith(publisherService);
verify(publisherService, user);
}
Bibliografia
Google Testing Blog
http://googletesting.blogspot.com/
Refactoring: Improving the Design of Existing Code (Martin Fowler)
http://www.refactoring.com/
Refactoring Workbook (William C. Wake)
http://xp123.com/rwb/
Applicare UML e Pattern (Craig Larman)
http://www.craiglarman.com
Principi di ingegneria del software (Pressman)
http://highered.mcgraw-hill.com/sites/0072853182/
Wikipedia
http://www.wikipedia.org
Strumenti utili
Unit Test:
www.junit.org
Test code coverage:
http://cobertura.sourceforge.net/
http://emma.sourceforge.net/
Testability:
http://code.google.com/p/testability-explorer/
http://testabilityexplorer.org/report
Mock objects:
http://www.easymock.org/
http://www.jmock.org/
http://code.google.com/p/mockito/
I nostri contatti
Davide Cerbo
[email protected]
[email protected]
http://jesty.it
Nicola Raglia
[email protected]
[email protected]
http://sourcengineering.org
Q&A
Q&A
Q&A
Scarica

public void - WordPress.com