III. Chapitre 2 - Gestion de l'authentification▲
Le besoin est le suivant :
- L'authentification sera effectuée en base de données
- L'utilisateur se connectera via son identifiant et son mot de passe
- La déconnexion sera également gérée.
III-A. Ecriture de la couche DAO▲
Nous allons créer les packages suivants :
- model pour les classes JavaBean
- dao pour la couche d'accès aux données
Lors de l'écriture de la couche métier et de la couche web, nous mettrons à jour la structure. L'idée est de préparer seulement les éléments nécessaires pour l'écriture de la couche DAO.
Nous rappelons les principes du processus du développement piloté par les tests :
- Ecriture du test qui doit correspondre à un besoin concret
- Exécution du test qui doit échouer car l'implémentation n'est pas encore faite
- Ecriture de l'implémentation
- Exécution du test qui doit passer
- Remaniement éventuel du test et de l'implémentation
III-A-1. Ecriture du test▲
Il est important que le test soit court et surtout lisible par n'importe quel développeur. Ce sera donc un test simple où l'on assertera que l'utilisateur authentifié existe et que son identifiant, prénom, nom et email sont JohnDoe, John, Doe, john.doe@abc.com, et qu'il n'est pas administrateur.
Rappel des annotations JUnit 4 :
- @RunWith permet de spécifier un TestClassRunner particulier : dans notre cas, nous utilisons SpringJUnit4ClassRunner afin de pouvoir utiliser l'injection de dépendances de Spring.
- @Test indique que la méthode est une méthode de test à exécuter
- @Before indique que la méthode doit être exécutée avant chaque test (équivalent du setUp() dans JUnit 3.8)
- @After indique que la méthode doit être exécutée après chaque test (équivalent du tearDown() dans JUnit 3.8)
public
class
SecurityDaoTest {
@Test
public
void
authenticateSuccess
(
) throws
Exception {
SecurityDao securityDao =
new
SecurityDaoImpl
(
);
User user =
securityDao.authenticate
(
"JohnDoe"
, "seven"
);
assertNotNull
(
"User should be not null"
, user);
assertEquals
(
"JohnDoe"
, user.getUserId
(
));
assertEquals
(
"John"
, user.getFirstName
(
));
assertEquals
(
"Doe"
, user.getLastName
(
));
assertEquals
(
"john.doe@abc.com"
, user.getEmail
(
));
assertFalse
(
user.isAdmin
(
));
}
}
Cette classe ne compile pas car il manque :
- Le Javabean User
- L'interface SecurityDao
- L'implémentation SecurityDaoImpl
Rectifions cela au plus vite ;) :
public
class
User implements
Serializable {
private
String userId;
private
String firstName;
private
String lastName;
private
String email;
private
String password;
private
boolean
isAdmin;
/* getters et setters ont été supprimés pour plus de clarté */
...
}
public
interface
SecurityDao {
public
User authenticate
(
String userId, String password);
}
public
class
SecurityDaoImpl implements
SecurityDao {
public
User authenticate
(
String userId, String password) throws
SQLException {
return
null
; // Todo
}
}
III-A-2. Exécution du test qui doit échouer▲
L'écriture du test est incomplète car il n'initialise pas la structure de la table et il ne met pas de données de test dans la base de données.
Mettons le à jour en utilisant DBUnit, framework de test qui offre des méthodes permettant d'interagir avec la base de données : Ordres SQL, comparaison de données de table, etc. Les données à insérer sont exprimables dans un fichier XML :
<dataset>
<table
name
=
"UTILISATEUR"
>
<column>
USER_ID</column>
<column>
FIRST_NAME</column>
<column>
LAST_NAME</column>
<column>
PASSWORD</column>
<column>
EMAIL</column>
<column>
IS_ADMIN</column>
<row>
<value>
JohnDoe</value>
<value>
John</value>
<value>
Doe</value>
<value>
seven</value>
<value>
john.doe@abc.com</value>
<value>
false</value>
</row>
</table>
</dataset>
Nous allons utiliser la fixture DatabaseTester de DBUnit. Il faut noter que DBUnit ne gère pas nativement le type BOOLEAN de Hslqdb. Il faudra modifier la configuration de la connexion DBUnit (Classe qui wrappe la connexion JDBC) :
public
class
SecurityDaoTest {
@Test
public
void
authenticateSuccess
(
) throws
Exception {
...
}
@Before
public
void
setUp
(
) throws
Exception {
IDatabaseTester databaseTester =
new
JdbcDatabaseTester
(
"org.hsqldb.jdbcDriver"
,
"jdbc:hsqldb:mem:mynotes"
,
"sa"
, ""
);
//grouille pour gérer le type BOOLEAN deHSQLDB
IDatabaseConnection dbUnitConnection =
databaseTester.getConnection
(
);
DatabaseConfig config =
dbUnitConnection.getConfig
(
);
config.setProperty
(
DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
new
HsqldbDataTypeFactory
(
));
DatabaseOperation.CLEAN_INSERT.execute
(
dbUnitConnection, getDataSet
(
));
}
private
IDataSet getDataSet
(
) throws
Exception {
return
new
XmlDataSet
(
getClass
(
).getResourceAsStream
(
"SecurityDaoTest.xml"
));
}
}
N'oublions pas d'écrire le code qui va initialiser notre base avec le fichier mynotes.ddl :
public
class
SecurityDaoTest {
@Test
public
void
authenticateSuccess
(
) throws
Exception {
...
}
@Before
public
void
setUp
(
) throws
Exception {
IDatabaseTester databaseTester =
new
JdbcDatabaseTester
(
"org.hsqldb.jdbcDriver"
,
"jdbc:hsqldb:mem:mynotes"
,
"sa"
, ""
);
//initialisation de la base de donnees
Connection connection =
dbUnitConnection.getConnection
(
);
Statement statement =
connection.createStatement
(
);
try
{
statement =
connection.createStatement
(
);
String sql =
readSqlFile
(
"/mynotes.ddl"
);
statement.execute
(
sql);
}
finally
{
statement.close
(
);
}
//grouille pour gérer le type BOOLEAN deHSQLDB
IDatabaseConnection dbUnitConnection =
databaseTester.getConnection
(
);
DatabaseConfig config =
dbUnitConnection.getConfig
(
);
config.setProperty
(
DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
new
HsqldbDataTypeFactory
(
));
DatabaseOperation.CLEAN_INSERT.execute
(
dbUnitConnection, getDataSet
(
));
}
private
String readSqlFile
(
String fileName) throws
FileNotFoundException {
StringBuilder strBuilder =
new
StringBuilder
(
);
Scanner scanner =
new
Scanner
(
new
File
(
getClass
(
).getResource
(
fileName).getFile
(
)));
while
(
scanner.hasNextLine
(
)) {
String line =
scanner.nextLine
(
);
strBuilder.append
(
line);
}
scanner.close
(
);
return
strBuilder.toString
(
);
}
}
Bon c'est un peu touffu, mais l'étape de remaniement n'intervient pas encore. Ré exécutons le afin de vérifier s'il fonctionne toujours, puis attaquons l'implémentation de la fonction d'authentification :
III-A-3. Implémentation de la fonction d'authentification en base▲
L'implémentation sera faite à l'aide du framework Ibatis qui propose une séparation franche entre le code java et le code SQL. Ce dernier est spécifié dans un fichier XML dont les requêtes SQL sont identifiées par un statementName. L'appel de ces requêtes se fait via cet identifiant, d'où l'appellation SQLMap (Mapping SQL).
Le fichier SQLMap Security.xml contiendra la requête d'authentification en base. Cette dernière retournera un objet User (attribut resultClass) et les paramètres d'entrée seront exprimés à l'aide d'une Map (attribut parameterClass). Notons que l'on peut définir un alias sur les classes utilisées dans le fichier pour alléger l'écriture des requêtes :
<?xml version="1.0" encoding="UTF-8" ?>
"http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap
namespace
=
"User"
>
<typeAlias
alias
=
"user"
type
=
"com.developpez.mynotes.model.User"
/>
<typeAlias
alias
=
"map"
type
=
"java.util.Map"
/>
<select
id
=
"authenticateUser"
resultClass
=
"user"
parameterClass
=
"map"
>
select
USER_ID as userId,
FIRSTNAME as firstName,
LASTNAME as lastName,
EMAIL as email
from UTILISATEUR
where USER_ID = #userId# and PASSWORD = #password#
</select>
</sqlMap>
Ensuite le fichier de configuration Ibatis sqlMapconfig.xml contiendra la configuration de la datasource et référencera notre fichier de mapping Security.xml :
<
?xml version=
"1.0"
encoding=
"UTF-8"
?>
<
sqlMapConfig>
<
properties resource=
"com/developpez/mynotes/dao/database.properties"
/>
<
transactionManager type=
"JDBC"
>
<
dataSource type=
"SIMPLE"
>
<
property name=
"JDBC.Driver"
value=
"${driver}"
/>
<
property name=
"JDBC.ConnectionURL"
value=
"${url}"
/>
<
property name=
"JDBC.Username"
value=
"${username}"
/>
<
property name=
"JDBC.Password"
value=
"${password}"
/>
</
dataSource>
</
transactionManager>
<
sqlMap resource=
"com/developpez/mynotes/dao/Security.xml"
/>
</
sqlMapConfig>
Le fichier de properties de la bdd database.properties contiendra les paramètres de la datasource :
driver
=
org.hsqldb.jdbcDriver
url
=
jdbc:hsqldb:mem: mynotes
username
=
sa
password
=
Enfin, l'implémentation de SecurityDao : Un SqlMapClient y sera instancié avec le fichier de configuration iBatis. L'appel du code SQL se fera à l'aide de la méthode de queryForObject("authenticateUser", parameters) :
public
class
SecurityDaoImpl implements
SecurityDao {
private
static
SqlMapClient sqlMap;
static
{
try
{
Reader reader =
getResourceAsReader
(
"com/developpez/mynotes/dao/sqlMapConfig.xml"
);
sqlMap =
SqlMapClientBuilder.buildSqlMapClient
(
reader);
}
catch
(
IOException e) {
System.out.println
(
"Erreur lors de l'initialisation du DAO = "
+
e.getMessage
(
));
}
}
public
User authenticate
(
String userId, String password) throws
SQLException {
Map<
String, String>
parameters =
new
HashMap<
String, String>(
);
parameters.put
(
"userId"
, userId);
parameters.put
(
"password"
, password);
return
(
User)sqlMap.queryForObject
(
"authenticateUser"
, parameters);
}
}
III-A-4. Ré-exécution du test▲
L'implémentation est terminée, nous pouvons relancer le test qui devrait réussir cette fois-ci :
Est-ce que tous les cas sont couverts ? Non, il manque le cas où l'utilisateur n'existe pas. Rajoutons quelques tests :
...
@Test
public
void
authenticateFailsWithUnknownUser
(
) throws
Exception {
SecurityDao securityDao =
new
SecurityDaoImpl
(
);
User user =
securityDao.authenticate
(
"John"
, "seven"
);
assertNull
(
"User should be null"
, user);
}
@Test
public
void
authenticateFailsWithNullParams
(
) throws
Exception {
User user =
securityDao.authenticate
(
null
, null
);
assertNull
(
"User should be null"
, user);
}
...
Exécutons-les tous :
III-A-5. Remaniement du test et de l'implémentation▲
Nous serions tentés de dire que la tâche est finie. Cependant nous devons prendre un peu de recul face au code et nous demander si un remaniement peut être effectué afin de le rendre plus simple, plus lisible, plus flexible.
Après analyse, nous pouvons noter les points suivants :
- Dans chaque méthode de test, l'implémentation de SecurityDao est instanciée. Nous pouvons en faire une variable d'instance. L'instanciation pourrait se faire dans une méthode d'initialisation setUp().
- Ensuite, le test est fortement couplé à l'implémentation de SecurityDao, il faudrait pouvoir le rendre indépendant et ce afin que le test puisse être exécuté sans modification du code java lors d'un changement d'implémentation
- Enfin, le test et l'implémentation devraient partager la même source de données.
Nous allons utiliser l'Inversion de Contrôle et de l'Injection de Dépendance avec le module spring-test. En effet, il propose des classes de test et des annotations compatibles JUnit 4.4 qui facilitent l'écriture des tests.
Ajoutons les dépendances nécessaires :
...
<dependency>
<groupId>
org.springframework</groupId>
<artifactId>
spring</artifactId>
<version>
2.5.5</version>
</dependency>
<dependency>
<groupId>
org.springframework</groupId>
<artifactId>
spring-test</artifactId>
<version>
2.5.5</version>
<scope>
test</scope>
</dependency>
...
Nous allons configurer la datasource via Spring avec le fichier suivant :
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean
id
=
"propertyConfigurer"
class
=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
>
<property
name
=
"location"
value
=
"classpath:/com/developpez/mynotes/dao/database.properties"
/>
</bean>
<!-- DAO Components Configuration -->
<bean
id
=
"dataSource"
class
=
"org.apache.commons.dbcp.BasicDataSource"
>
<property
name
=
"driverClassName"
value
=
"${driver}"
/>
<property
name
=
"url"
value
=
"${url}"
/>
<property
name
=
"username"
value
=
"${username}"
/>
<property
name
=
"password"
value
=
"${password}"
/>
</bean>
<!-- DAO Stack -->
<bean
id
=
"securityDao"
class
=
"com.developpez.mynotes.dao.SecurityDaoImpl"
>
</bean>
</beans>
Ensuite réécrivons le test unitaire à la sauce Spring JUnit 4 avec les annotations Spring :
- @ContextConfiguration(locations = ...) précise les fichiers de configuration Spring
- @Autowired indique à Spring les variables de classes sur lesquelles il va rattacher les instances des objets déclarés dans les fichiers de configuration.
Par exemple, dans notre classe de test, nous indiquons que la variable d'instance securityDao accueillera une implémentation de SecurityDao. Il en sera de même pour notre DatabaseTester
Nous en profitons pour découper l'initialisation en petites méthodes afin d'améliorer la lecture :
@RunWith
(
SpringJUnit4ClassRunner.class
)
@ContextConfiguration
(
locations =
{
"applicationContext-dao.xml"
, "applicationContext-dao-test.xml"
}
)
public
class
SecurityDaoTest {
@Autowired
private
SecurityDao securityDao;
@Autowired
private
DataSourceDatabaseTester databaseTester;
@Test
public
void
authenticateSucceed
(
) throws
Exception {
User user =
securityDao.authenticate
(
"JohnDoe"
, "seven"
);
...
}
//Autres tests
...
@Before
public
void
setUp
(
) throws
Exception {
initDatabase
(
);
initDataSet
(
);
}
private
void
initDatabase
(
) throws
Exception {
IDatabaseConnection dbUnitConnection =
databaseTester.getConnection
(
);
Connection connection =
dbUnitConnection.getConnection
(
);
Statement statement =
connection.createStatement
(
);
try
{
statement =
connection.createStatement
(
);
String sql =
readSqlFile
(
"/mynotes.ddl"
);
statement.execute
(
sql);
}
finally
{
statement.close
(
);
}
}
private
void
initDataSet
(
) throws
Exception {
IDatabaseConnection dbUnitConnection =
databaseTester.getConnection
(
);
DatabaseConfig config =
dbUnitConnection.getConfig
(
);
config.setProperty
(
DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
new
HsqldbDataTypeFactory
(
));
DatabaseOperation.CLEAN_INSERT.execute
(
dbUnitConnection, getDataSet
(
));
}
private
XmlDataSet getDataSet
(
) throws
DataSetException {
...
}
private
String readSqlFile
(
String fileName) throws
FileNotFoundException {
...
}
}
Le fichier applicationContext-dao-test.xml contiendra la déclaration du bean DatabaseTester :
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean
id
=
"databaseTester"
class
=
"org.dbunit.DataSourceDatabaseTester"
>
<constructor-arg
index
=
"0"
ref
=
"dataSource"
/>
</bean>
</beans>
Ré-exécutons les tests :
En relisant le test, cela reste encore un peu lourd. Nous allons extraire une classe abstraite qui sera réutilisée pour nos futurs scénarii. L'élément variable finalement est le fichier SecurityDaoTest.xml. Nous déclarerons donc une méthode abstraite getDataSetFile(). Enfin, le fichier applicationContext-dao-test.xml sera déclaré dans cette classe.
@RunWith
(
SpringJUnit4ClassRunner.class
)
@ContextConfiguration
(
locations =
{
"applicationContext-dao-test.xml"
}
)
public
abstract
class
AbstractDaoTestCase {
@Autowired
private
DataSourceDatabaseTester databaseTester;
protected
abstract
String getDataSetFile
(
);
@Before
public
void
setUp
(
) throws
Exception {
initDatabase
(
);
initDataSet
(
);
}
private
void
initDatabase
(
) throws
Exception {
...
}
private
void
initDataSet
(
) throws
Exception {
...
}
private
XmlDataSet getDataSet
(
) throws
DataSetException {
...
}
private
String readSqlFile
(
String fileName) throws
FileNotFoundException {
...
}
}
En conséquence, la classe de test SecurityDaoTest subira quelques modifications :
@ContextConfiguration
(
locations =
{
"applicationContext-dao.xml"
}
)
public
class
SecurityDaoTest extends
AbstractDaoTestCase {
@Autowired
private
SecurityDao securityDao;
@Test
public
void
authenticateSucceed
(
) throws
Exception {
...
}
@Test
public
void
authenticateFailsWithUnkownUser
(
) throws
Exception {
...
}
@Test
public
void
authenticateFailsWithNullParams
(
) throws
Exception {
...
}
@Override
protected
String getDataSetFile
(
) {
return
"SecurityDaoTest.xml"
;
}
}
Les tests passent encore. La classe de test SecurityDaoTest semble plus simple à lire. L'importance d'extraire des classes abstraites est que cela permet de mettre en évidence et d'isoler un comportement redondant. Par ailleurs, lorsque le développeur écrira une nouvelle classe de test, il se focalisera sur l'écriture de ses tests et ne perdra pas de temps à mettre en place la configuration de leur exécution.
A présent, nous allons remanier l'implémentation du DAO : la classe SqlMapClient devrait à présent être gérée par Spring et la datasource devrait être aussi utilisée par l'implémentation.
Mise à jour du fichier applicationContext-dao.xml :
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean
id
=
"propertyConfigurer"
class
=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
>
<property
name
=
"location"
value
=
"classpath:/com/developpez/mynotes/dao/database.properties"
/>
</bean>
<!-- DAO Components Configuration -->
<bean
id
=
"dataSource"
class
=
"org.apache.commons.dbcp.BasicDataSource"
>
<property
name
=
"driverClassName"
value
=
"${driver}"
/>
<property
name
=
"url"
value
=
"${url}"
/>
<property
name
=
"username"
value
=
"${username}"
/>
<property
name
=
"password"
value
=
"${password}"
/>
</bean>
<bean
id
=
"sqlMapClient"
class
=
"org.springframework.orm.ibatis.SqlMapClientFactoryBean"
>
<property
name
=
"configLocation"
>
<value>
classpath:/com/developpez/mynotes/dao/sqlMapConfig.xml</value>
</property>
<property
name
=
"dataSource"
>
<ref
bean
=
"dataSource"
/>
</property>
</bean>
<!-- DAO Stack -->
<bean
id
=
"securityDao"
class
=
"com.developpez.mynotes.dao.SecurityDaoImpl"
>
<property
name
=
"sqlMapClient"
>
<ref
bean
=
"sqlMapClient"
/>
</property>
</bean>
</beans>
Le fichier sqlMapConfig.xml se simplifie en ne référençant que les fichiers de mapping SQL :
<?xml version="1.0" encoding="UTF-8" ?>
<sqlMapConfig>
<sqlMap
resource
=
"com/developpez/mynotes/dao/Security.xml"
/>
</sqlMapConfig>
Ensuite utilisons le template Ibatis proposé par spring-orm pour mettre à jour la classe SecurityDaoImpl :
public
class
SecurityDaoImpl extends
SqlMapClientTemplate implements
SecurityDao {
public
User authenticate
(
String userId, String password) throws
SQLException {
Map<
String, String>
parameters =
new
HashMap<
String, String>(
);
parameters.put
(
"userId"
, userId);
parameters.put
(
"password"
, password);
return
(
User)queryForObject
(
"authenticateUser"
, parameters);
}
}
Nous pouvons remarquer que l'implémentation se simplifie grandement.
Pour ceux qui préfèrent garder l'indépendance du code applicatif vis-à-vis de Spring, l'implémentation sera presque similaire à la première version :
public
class
SecurityDaoImpl implements
SecurityDao {
private
SqlMapClient sqlMapClient;
public
User authenticate
(
String userId, String password) throws
SQLException {
Map<
String, String>
parameters =
new
HashMap<
String, String>(
);
parameters.put
(
"userId"
, userId);
parameters.put
(
"password"
, password);
return
(
User)sqlMapClient.queryForObject
(
"authenticateUser"
, parameters);
}
public
void
setSqlMapClient
(
SqlMapClient sqlMapClient) {
this
.sqlMapClient =
sqlMapClient;
}
}
Ré-exécutons les tests unitaires qui doivent toujours passer :
Résumé des tâches effectuées pour la couche Dao :
- Nous avons écrit le test unitaire de l'authentification BDD avec DBUnit,
- Nous avons exécuté le test qui a échoué,
- Nous avons implémenté une première version de SecurityDaoImpl avec Ibatis,
- Le test est passé,
- Nous avons intégré spring-test pour la classe de test et nous avons extrait une classe de test abstraite,
- Le test a continué à passer,
- Nous avons intégré spring-orm pour l'implémentation SecurityDaoImpl,
- Le test a continué à passer
Ce qui est important de retenir est que nous avons modelé notre code au fur est à mesure. Un des principes d'XP est qu'il faut d'abord produire un code qui marche avant de penser à son remaniement/optimisation éventuel ("Do The Simplest Thing That Could Possibly Work"). Le test unitaire garantit la maîtrise du processus du développement (on sait rapidement où et quand le bug apparaît) et la robustesse du code.
III-B. Tâche 2 - Ecriture de la couche Business▲
La couche Business ou Service est celle où " en général " sont effectués les traitements fonctionnels. Par exemple, lors de l'envoi d'un email, il y a l'opération " Envoyer un email via SMTP ", puis stockage de l'email dans la base de données. Nous allons mettre à jour de la structure en rajoutant un package business sous com.developpez.mynotes.
III-B-1. Ecriture du test unitaire▲
Il sera très similaire à celui de la couche DAO :
public
class
SecurityServiceTest {
@Test
public
void
authenticateSuccess
(
) throws
Exception {
SecurityService securityService =
new
SecurityServiceImpl
(
);
User user =
securityService.authenticate
(
"JohnDoe"
, "seven"
);
assertNotNull
(
"User should not be null"
,user);
}
}
Il nous manque l'interface SecurityService et son implémentation SecurityServiceImpl :
public
interface
SecurityService {
public
User authenticate
(
String userId, String password) throws
Exception;
}
public
class
SecurityServiceImpl implements
SecurityService {
public
User authenticate
(
String userId, String password) throws
Exception {
return
null
;
}
}
III-B-2. Exécution du test qui doit échouer▲
III-B-3. Implémentation▲
Ce sera très simple. Il suffit d'invoquer la méthode authenticate(String, String) de SecurityDao :
public
class
SecurityServiceImpl implements
SecurityService {
private
SecurityDao securityDao;
public
User authenticate
(
String userId, String password) throws
Exception {
return
securityDao.authenticate
(
userId, password);
}
}
III-B-4. Ré-exécution du test▲
Dans le cas d'une petite application, nous pourrions directement appeler l'implémentation de SecurityDao. L'ordre d'écriture des couches est arbitraire mais imaginons que l'implémentation de SecurityDao n'est pas prête, il faut pouvoir avancer malgré ce handicap. Nous allons utiliser la technique de mocking (simulation).
Les Mock (simulacres) sont utilisés pour pouvoir tester une fonctionnalité sans devoir utiliser et configurer tous les modules externes avec qui elle doit communiquer. On simule les réponses attendues en fonction de conditions prédéfinies. Dans l'exemple de l'envoi d'un email, on simulerait l'envoi et la réponse du serveur SMTP.
Notre test veut récupérer un utilisateur JohnDoe dont le mot de passe est 'seven'. Implémentons notre mock à la "mano" ;) :
public
class
SecurityDaoMock implements
SecurityDao{
public
User authenticate
(
String userId, String password) throws
Exception {
return
null
; // Todo
}
}
Nous allons créer un dictionnaire d'utilisateur indexé sur son identifiant :
public
class
SecurityDaoMock implements
SecurityDao{
private
static
final
Map<
String, User>
users =
new
HashMap<
String, User>(
);
static
{
users.put
(
"JohnDoe"
,
buildUser
(
"JohnDoe"
, "John"
, "Doe"
, "john.doe@abc.com"
, "seven"
, false
));
}
public
User authenticate
(
String userId, String password) throws
SQLException {
User user =
users.get
(
userId);
if
(
user !=
null
&&
user.getPassword
(
).equals
(
password)) {
return
user;
}
return
null
;
}
private
static
User buildUser
(
String userId, String firstName, String lastName, String email,
String password, boolean
isAdmin) {
User user =
new
User
(
);
user.setUserId
(
userId);
user.setFirstName
(
firstName);
user.setLastName
(
lastName);
user.setEmail
(
email);
user.setPassword
(
password);
user.setAdmin
(
isAdmin);
return
user;
}
}
A présent, ajoutons un setter sur securityDao de SecurityServiceImpl :
public
class
SecurityServiceImpl implements
SecurityService {
...
public
void
setSecurityDao
(
SecurityDao securityDao) {
this
.securityDao =
securityDao;
}
}
Dans notre test, settons le mock :
public
class
SecurityServiceTest {
@Test
public
void
authenticateSuccess
(
) throws
Exception {
SecurityService securityService =
new
SecurityServiceImpl
(
);
((
SecurityServiceImpl) securityService).setSecurityDao
(
new
SecurityDaoMock
(
));
User user =
securityService.authenticate
(
"JohnDoe"
, "seven"
);
assertNotNull
(
"User should not be null"
, user);
}
}
Enfin, exécutons notre test :
Nous pouvons rajouter les tests d'échec et les exécuter :
public
class
SecurityServiceTest {
@Test
public
void
authenticateSuccess
(
) throws
Exception {
SecurityService securityService =
new
SecurityServiceImpl
(
);
((
SecurityServiceImpl)securityService).setSecurityDao
(
new
SecurityDaoMock
(
));
User user =
securityService.authenticate
(
"JohnDoe"
, "seven"
);
assertNotNull
(
"User should not be null"
, user);
}
@Test
public
void
authenticateFailsWithBadPassword
(
) throws
Exception {
SecurityService securityService =
new
SecurityServiceImpl
(
);
((
SecurityServiceImpl)securityService).setSecurityDao
(
new
SecurityDaoMock
(
));
User user =
securityService.authenticate
(
"JohnDoe"
, "six"
);
assertNull
(
"User should not be null"
, user);
}
@Test
public
void
authenticateFailsWithUnknownUser
(
) throws
Exception {
SecurityService securityService =
new
SecurityServiceImpl
(
);
((
SecurityServiceImpl)securityService).setSecurityDao
(
new
SecurityDaoMock
(
));
User user =
securityService.authenticate
(
"JohnLennon"
, "imagine"
);
assertNull
(
"User should be null"
, user);
}
@Test
public
void
authenticateFailsWithNullParams
(
) throws
Exception {
SecurityService securityService =
new
SecurityServiceImpl
(
);
((
SecurityServiceImpl)securityService).setSecurityDao
(
new
SecurityDaoMock
(
));
User user =
securityService.authenticate
(
null
, null
);
assertNull
(
"User should be null"
, user);
}
}
III-B-5. Remaniement du test▲
L'implémentation étant simple, nous allons nous focaliser sur le remaniement du test. Nous pouvons laisser spring gérer les dépendances en écrivant 2 configurations :
Nous reprenons notre fichier applicationContext-dao-test.xml en lui ajoutant notre mock :
<?xml version="1.0" encoding="UTF-8"?>
<beans>
...
<!-- DAO Stack -->
<bean
id
=
"securityDao"
class
=
"com.developpez.mynotes.dao.SecurityDaoMock"
>
</bean>
</beans>
Le fichier applicationContext-business.xml contiendra la déclaration de notre service :
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean
id
=
"securityService"
class
=
"com.developpez.mynotes.business.SecurityServiceImpl"
>
<property
name
=
"securityDao"
>
<ref
bean
=
"securityDao"
/>
</property>
</bean>
</beans>
Mettons à jour le test pour qu'il se lance dans un contexte Spring :
@RunWith
(
SpringJUnit4ClassRunner.class
)
@ContextConfiguration
(
locations =
{
"applicationContext-business.xml"
, "../dao/applicationContext-dao-test.xml"
}
)
public
class
SecurityServiceTest {
@Autowired
private
SecurityService securityService;
@Test
public
void
authenticateSuccess
(
) throws
Exception {
User user =
securityService.authenticate
(
"JohnDoe"
, "seven"
);
assertNotNull
(
"User should not be null"
, user);
}
@Test
public
void
authenticateFailsWithBadPassword
(
) throws
Exception {
User user =
securityService.authenticate
(
"JohnDoe"
, "six"
);
assertNull
(
"User should not be null"
, user);
}
@Test
public
void
authenticateFailsWithUnknownUser
(
) throws
Exception {
User user =
securityService.authenticate
(
"JohnLennon"
, "imagine"
);
assertNull
(
"User should be null"
, user);
}
@Test
public
void
authenticateFailsWithNullParams
(
) throws
Exception {
User user =
securityService.authenticate
(
null
, null
);
assertNull
(
"User should be null"
, user);
}
}
Ainsi en couplant les 2 configurations, Spring rattache le securityDaoMock à la variable securityDao de SecurityService. Nous pouvons ré-exécuter les tests :
III-B-6. Remaniement du test (Encore ?!)▲
Nous pouvons nous passer de l'écriture d'un mock en utilisant des API dédiées. Plusieurs librairies de mock existent, les plus célèbres sont EasyMock, JMock. Nous allons étudier la librairie Mockito dont la syntaxe est très plaisante.
Cette librairie permet d'intercepter l'appel d'une méthode d'une classe et de simuler le renvoi spécifique d'un résultat en fonction des paramètres fournis. Dans le vocabulaire Mockito, c'est un stub (bouchon).
Dans notre cas nous avons besoin de simuler authenticate(String, String) de SecurityDao.
Tout d'abord, mettons à jour notre pom en rajoutant la dépendance Mockito :
...
<dependency>
<groupId>
org.mockito</groupId>
<artifactId>
mockito</artifactId>
<version>
1.5</version>
<scope>
test</scope>
</dependency>
...
Ensuite nous allons modifier la classe de test. Pour cela nous déclarons le mock avec l'annotation @Mock. Ensuite dans une méthode @Before, nous allons remplacer le dao par le mock et les stubs correspondant aux tests :
@RunWith
(
SpringJUnit4ClassRunner.class
)
@ContextConfiguration
(
locations =
{
"applicationContext-business.xml"
, "../dao/applicationContext-dao.xml"
}
)
public
class
SecurityServiceTest {
@Mock
private
SecurityDao securityDaoMock;
@Autowired
private
SecurityService securityService;
... // les tests ont été enlevés ici pour plus de clarté
@Before
public
void
setUp
(
) throws
SQLException {
MockitoAnnotations.initMocks
(
this
);
ReflectionTestUtils.setField
(
securityService, "securityDao"
, securityDaoMock);
Mockito.stub
(
securityDaoMock.authenticate
(
"JohnDoe"
, "seven"
))
.toReturn
(
buildUser
(
"JohnDoe"
, "John"
, "Doe"
, "john.doe@abc.com"
, "seven"
, false
));
}
private
static
User buildUser
(
String userId, String firstName, String lastName, String email,
String password, boolean
isAdmin) {
User user =
new
User
(
);
user.setUserId
(
userId);
user.setFirstName
(
firstName);
user.setLastName
(
lastName);
user.setEmail
(
email);
user.setPassword
(
password);
user.setAdmin
(
isAdmin);
return
user;
}
}
Nous avons donc indiqué à la méthode stub() que lorsque la méthode authenticate() du SecurityDaoMock est invoquée avec les paramètres "JohnDoe" et "seven" alors il faut retourner un objet User (via la méthode toReturn() ). Dans les autres cas, Mockito retourne null.
Nous pouvons remarquer l'indépendance des tests par rapport à l'implémentation de la classe à tester. Cela évite l'écriture d'un mock et d'une configuration Spring spécifique au test.
III-C. Tache 3 : Ecriture de la couche Web▲
A partir de cette tâche, nous utiliserons directement Spring pour les tests et pour la configuration, mais rien ne vous empêche d'écrire le test et le code en plusieurs versions ;).
Nous allons mettre à jour la structure en rajoutant un package web sous com.developpez.web et nous rajouter les dépendances Wicket dans le pom.xml :
...
<dependency>
<groupId>
org.apache.wicket</groupId>
<artifactId>
wicket</artifactId>
<version>
1.3.4</version>
</dependency>
<dependency>
<groupId>
org.apache.wicket</groupId>
<artifactId>
wicket-ioc</artifactId>
<version>
1.3.4</version>
<exclusions>
<exclusion>
<groupId>
org.springframework</groupId>
<artifactId>
spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>
org.apache.wicket</groupId>
<artifactId>
wicket-extensions</artifactId>
<version>
1.3.4</version>
</dependency>
<dependency>
<groupId>
org.apache.wicket</groupId>
<artifactId>
wicket-spring-annot</artifactId>
<version>
1.3.4</version>
</dependency>
...
Une remarque importante sur le couple Wicket + Spring :
Sur le site officiel, Erik Van Oosten explique la difficulté qu'il a eu lorsqu'il a voulu ajouter
le support de Spring dans Wicket :
- Wicket ne gère pas le cycle de vie de ses composants car une page (ou composant) peut être instancié(e) à n'importe quel moment dans le code applicatif, ce qui rend difficile l'injection de dépendance car Spring ne peut pas intercepter la création du composant dans ce cas là.
- Les composants et modèles Wicket sont souvent sérialisés. Il explique que Wicket conserve la hiérarchie des composants en session (un des inconvénients de wicket). Dans un environnement distribué, les données en session sont répliquées de cluster en cluster par le mécanisme de sérialisation/désérialisation. Si l'injection de dépendance est appliquée, en remontant tout l'arbre des dépendances des composants à sérialiser, on serait presque amené à sérialiser le container Wicket et, lors de la phase de désérialisation, on obtiendrait un clone du container et non le container d'origine ce qui n'est pas acceptable.
Plusieurs solutions ont été envisagées, celle qui semble simple à mettre en oeuvre est d'utiliser le module wicket-spring-annot avec l'annotation spécifique @SpringBean et la classe SpringComponentInjector.
- SpringComponentInjector est une classe qui va récupérer le contexte Spring de l'application.
- @SpringBean indiquera à SpringComponentInjector les champs à setter.
Nous allons découper la tâche en 3 sous-tâches :
- Ecriture de la page d'accueil
- Gestion de l'authentification de la page d'accueil
- Affichage de la page principale après connexion
III-C-1. Ecriture de la page d'accueil▲
III-C-1-a. Ecriture du test▲
Nous allons d'abord faire un test sur la page d'accueil par défaut : cette page contiendra un petit formulaire de connexion.
Wicket propose quelques classes très sympatiques
- ApplicationContextMock est une classe qui simule l'ApplicationContext de Spring et qui permet de rajouter des beans dans le container.
- WicketTester est une fixture qui possède des méthodes d'assertion intéressantes comme le rendu de la page, la présence de composants et des méthodes de saisie de formulaire.
Nous allons donc asserter que notre HomePage s'affiche bien et que les composants userId de type TextField et password de type PasswordTextField existent bien dans la page :
public
class
HomePageTest {
protected
WicketTester tester;
@Test
public
void
testRender
(
) throws
Exception {
tester.startPage
(
HomePage.class
);
tester.assertRenderedPage
(
HomePage.class
);
tester.assertComponent
(
"loginForm:userId"
, TextField.class
);
tester.assertComponent
(
"loginForm:password"
, PasswordTextField.class
);
tester.assertComponent
(
"loginForm:submit"
, Button.class
);
}
@Before
public
void
setUp
(
) throws
Exception {
tester =
new
WicketTester
(
new
MyNotesApplication
(
));
}
}
Pour que la classe de test compile, il nous manque :
- La classe MyNotesApplication qui est la servlet de l'application
- La classe HomePage et son fichier html
public
class
MyNotesApplication extends
WebApplication {
@Override
public
Class getHomePage
(
) {
return
HomePage.class
;
}
}
public
class
HomePage extends
WebPage {
}
III-C-1-b. Exécution du test qui doit échouer▲
III-C-1-c. Implémentation▲
Nous devons ajouter, dans notre page, un formulaire avec un champ de saisie normal pour l'identifiant, un champ de saisie masquée pour le mot de passe, et un bouton de validation.
Chaque composant ajouté possède un identifiant qui servira à la déclaration du composant dans la page html. L'appel de la méthode setModel() avec un CompoundPropertyModel indique à Wicket qu'il va setter par introspection les attributs userId et password avec les valeurs des champs du formulaire.
public
class
HomePage extends
WebPage {
public
HomePage
(
) {
add
(
new
LoginForm
(
"loginForm"
));
}
public
class
LoginForm extends
Form {
private
String userId;
private
String password;
public
LoginForm
(
String id) {
super
(
id);
setModel
(
new
CompoundPropertyModel
(
this
));
add
(
new
Label
(
"loginTitle"
, "MyNotes - Connexion"
));
add
(
new
TextField
(
"userId"
));
add
(
new
PasswordTextField
(
"password"
));
Button button =
new
Button
(
"loginSubmit"
) {
@Override
public
void
onSubmit
(
) {
}
}
;
add
(
button);
}
//getter et setter de userId et password
...
}
}
Ensuite composons la vue html :
<html>
<head>
<title>MyNotes</title>
</head>
<body>
<center>
<table cellpadding
=
"0"
cellspacing
=
"0"
>
<tr>
<td>
<form wicket
:
id
=
"loginForm"
>
<table>
<th><td><span wicket
:
id
=
"loginTitle"
>
Connexion</span></td></th>
<tr>
<td>Identifiant :</td>
<td><input wicket
:
id
=
"userId"
name
=
"userId"
type
=
"text"
size
=
"50"
/></td>
</tr>
<tr>
<td>Mot de passe :</td>
<td><input wicket
:
id
=
"password"
name
=
"password"
type
=
"password"
size
=
"10"
/></td>
</tr>
<tr>
<td></td>
<td><input wicket
:
id
=
"loginSubmit"
type
=
"submit"
value
=
"Connexion"
></td>
</tr>
</table>
</form>
</td>
</tr>
</table>
</center>
</body>
</html>
III-C-1-d. Ré-exécution du test▲
III-C-2. Gestion de l'authentification de la page d'accueil▲
Une fois, l'utilisateur authentifié, un menu apparaîtra à gauche et un texte d'accueil personnalisé à droite. Dans le menu, il y aura la déconnexion seulement.
III-C-2-a. Ecriture du test▲
Nous allons créer un nouveau test pour une page nommée LoggedInPage :
public
class
LoggedInPageTest {
protected
WicketTester tester;
@Test
public
void
testRender
(
) throws
Exception {
tester.startPage
(
HomePage.class
);
FormTester formTester =
tester.newFormTester
(
"loginForm"
);
formTester.setValue
(
"userId"
, "JohnDoe"
);
formTester.setValue
(
"password"
, "seven"
);
formTester.submit
(
"submit"
);
tester.assertNoErrorMessage
(
);
tester.assertRenderedPage
(
LoggedInPage.class
);
}
@Before
public
void
setUp
(
) throws
Exception {
tester =
new
WicketTester
(
new
MyNotesApplication
(
));
}
}
III-C-2-b. Exécution du test qui doit échouer▲
III-C-2-c. Implémentation▲
Il nous faut donc implémenter l'authentification via le bouton submit. Il aura une référence vers SecurityService et dont l'implémentation sera gérée via Spring.
Dans la même situation que dans la tâche précédente, il se peut que l'implémentation de SecurityService ne soit pas disponible. Aussi nous pouvons mocker l'interface avec Mockito afin de pouvoir tester notre page. Mettons à jour la classe LoginForm de HomePage :
...
@SpringBean
private
SecurityService securityService;
...
Button button =
new
Button
(
"submit"
) {
@Override
public
void
onSubmit
(
) {
User user;
try
{
user =
securityService.authenticate
(
getUserId
(
), getPassword
(
));
if
(
user ==
null
) {
error
(
"Identifiant ou mot de passe invalide."
);
}
else
{
MyNotesSession session =
(
MyNotesSession)getPage
(
).getSession
(
);
session.setUser
(
user);
setResponsePage
(
LoggedInPage.class
);
}
}
catch
(
Exception e) {
error
(
e.getMessage
(
));
}
}
}
;
...
Où SecurityService est déclaré en tant que bean Spring.
Lorsque l'objet user est renvoyé par la couche service, s'il n'existe pas il faut renvoyer un message d'erreur sinon il faut le stocker dans la session de l'utilisateur. Nous devons donc étendre la classe WebSession afin de pouvoir garder une référence d'objet user. Elle sera nommée MyNotesSession :
public
class
MyNotesSession extends
WebSession {
private
User user;
public
MyNotesSession
(
Request request) {
super
(
request);
}
public
void
setUser
(
User user) {
this
.user =
user;
}
public
User getUser
(
) {
return
user;
}
}
Notre MyNotesApplication devra donc surcharger la méthode newSession afin de retourner une instance de MyNotesSession :
public
class
MyNotesApplication extends
WebApplication {
@Override
public
Class getHomePage
(
) {
return
HomePage.class
;
}
@Override
public
MyNotesSession newSession
(
Request request, Response response) {
return
new
MyNotesSession
(
request);
}
...
}
Notre page d'accueil intégrera un FeedbackPanel qui permet d'afficher les erreurs remontées par les pages web (comme la balise <errors/> de Struts). Du coup, il faut mettre à jour HomePageTest.java, HomePage.java, HomePage.html :
public
class
HomePageTest {
protected
WicketTester tester;
@Test
public
void
testRender
(
) throws
Exception {
tester.startPage
(
HomePage.class
);
tester.assertRenderedPage
(
HomePage.class
);
tester.assertComponent
(
"feedback"
, FeedbackPanel.class
);
tester.assertComponent
(
"loginForm:userId"
, TextField.class
);
tester.assertComponent
(
"loginForm:password"
, PasswordTextField.class
);
tester.assertComponent
(
"loginForm:submit"
, Button.class
);
}
...
}
public
class
HomePage extends
WebPage {
public
HomePage
(
) {
add
(
new
LoginForm
(
"loginForm"
));
add
(
new
FeedbackPanel
(
"feedback"
));
}
...
}
<html>
<head>
<title>MyNotes</title>
</head>
<body>
<center>
<table cellpadding
=
"0"
cellspacing
=
"0"
>
<tr>
<td>
<span wicket
:
id
=
"feedback"
/>
</td>
</tr>
<tr>
<td>
<form wicket
:
id
=
"loginForm"
>
...
</form>
</td>
</tr>
</table>
</center>
</body>
</html>
Enfin nous allons mocker l'interface SecurityService et le contexte Spring dans notre classe de test :
public
class
LoggedInPageTest {
protected
WicketTester tester;
@Mock
private
SecurityService securityService;
@Test
public
void
testRender
(
) throws
Exception {
...
}
@Before
public
void
setUp
(
) throws
Exception {
final
ApplicationContextMock applicationContextMock =
new
ApplicationContextMock
(
);
MockitoAnnotations.initMocks
(
this
);
Mockito.stub
(
securityService.authenticate
(
"JohnDoe"
, "seven"
))
.toReturn
(
UserFactory.buildUser
(
"JohnDoe"
, "John"
, "Doe"
, "johndoe@abc.com"
, "seven"
, false
));
applicationContextMock.putBean
(
"securityService"
, securityService);
MyNotesApplication webApp =
new
MyNotesApplication
(
) {
@Override
public
void
init
(
) {
addComponentInstantiationListener
(
new
SpringComponentInjector
(
this
, applicationContextMock));
}
}
;
tester =
new
WicketTester
(
webApp);
}
}
Nous pouvons noter que :
- Nous avons surchargé spécifiquement la méthode init() de MyNotesApplication dans cette classe de test pour pouvoir introduire notre ApplicationContextMock dans notre servlet.
- La méthode buildUser a été mise dans une classe utilitaire nommée UserFactory afin que SecurityDaoMock, SecurityServiceTest (version Mockito) et LoggedInPageTest puissent l'utiliser.
public
class
UserFactory {
private
UserFactory
(
) {
}
public
static
User buildUser
(
String userId, String firstName, String lastName,
String email, String password, boolean
isAdmin) {
User user =
new
User
(
);
user.setUserId
(
userId);
user.setFirstName
(
firstName);
user.setLastName
(
lastName);
user.setEmail
(
email);
user.setPassword
(
password);
user.setAdmin
(
isAdmin);
return
user;
}
}
III-C-2-d. Exécution du test▲
III-C-3. Affichage de la page principale après connexion▲
Nous allons mettre à jour le test en assertant nos composants web :
- L'hyperlien de déconnexion
- Le nom complet de l'utilisateur sera compris dans le message d'accueil (WicketTester ne peut que tester ses composants, et il est préférable de laisser le texte dans le code html plutôt que dans le code java)
III-C-3-a. Ecriture du test▲
public
class
LoggedInPageTest {
protected
WicketTester tester;
@Mock
private
SecurityService securityService;
@Test
public
void
testRender
(
) throws
Exception {
tester.startPage
(
HomePage.class
);
FormTester formTester =
tester.newFormTester
(
"loginForm"
);
formTester.setValue
(
"userId"
, "JohnDoe"
);
formTester.setValue
(
"password"
, "seven"
);
formTester.submit
(
"submit"
);
tester.assertNoErrorMessage
(
);
tester.assertRenderedPage
(
LoggedInPage.class
);
tester.assertComponent
(
"logoutLink"
, Link.class
);
tester.assertLabel
(
"user"
, "John Doe"
);
}
...
}
III-C-3-b. Exécution du test▲
III-C-3-c. Implémentation▲
Nous allons ajouter à la page LoggedInPage :
- L'hyperlien LogoutLink qui invalidera la session de l'utilisateur et redirigera le navigateur vers la page de connexion,
- Un Label contenant le nom de l'utilisateur pour personnaliser le message d'accueil.
public
class
LoggedInPage extends
WebPage {
public
LoggedInPage
(
) {
add
(
new
LogoutLink
(
));
User user =
((
MyNotesSession)getPage
(
).getSession
(
)).getUser
(
);
add
(
new
Label
(
"user"
, user.getFullName
(
)));
}
private
class
LogoutLink extends
Link {
private
LogoutLink
(
) {
super
(
"logoutLink"
);
}
@Override
public
void
onClick
(
) {
Session.get
(
).invalidateNow
(
);
setResponsePage
(
Application.get
(
).getHomePage
(
));
}
}
}
<html>
<head>
<title>MyNotes</title>
</head>
<body>
<center>
<table cellpadding
=
"0"
cellspacing
=
"0"
>
<tr>
<td>
<a wicket
:
id
=
"logoutLink"
name
=
"logoutLink"
><span wicket
:
id
=
"label"
>
logout</span></a>
</td>
<td>
Bonjour <span wicket
:
id
=
"user"
></span>
et bienvenue dans l'application MyNotes.
</td>
</tr>
</table>
</center>
</body>
</html>
III-C-3-d. Exécution du test▲
Nous pouvons rajouter un test d'erreur de connexion :
...
@Test
public
void
testAuthenticateFailsWithMissingInput
(
) throws
Exception {
tester.startPage
(
HomePage.class
);
FormTester formTester =
tester.newFormTester
(
"loginForm"
);
formTester.submit
(
"submit"
);
tester.assertErrorMessages
(
new
String[] {
"Le champ 'userId' est obligatoire."
,
"Le champ 'password' est obligatoire."
,
}
);
}
@Test
public
void
testAuthenticateFailsWithUnknownUser
(
) throws
Exception {
tester.startPage
(
HomePage.class
);
FormTester formTester =
tester.newFormTester
(
"loginForm"
);
formTester.setValue
(
"userId"
, "JohnDoe"
);
formTester.setValue
(
"password"
, "six"
);
formTester.submit
(
"submit"
);
tester.assertErrorMessages
(
new
String[] {
"Identifiant ou mot de passe invalide."
}
);
}
...
Après ré-exécution, nous obtenons les résultats suivants :
Nous nous rendons compte que le champ userId n'a pas été rendu obligatoire : changeons les propriétés du TextField associé dans la classe UserForm :
...
public
LoginForm
(
String id) {
super
(
id);
setModel
(
new
CompoundPropertyModel
(
this
));
add
(
new
Label
(
"loginTitle"
, "MyNotes - Connexion"
));
TextField userIdTextField =
new
TextField
(
"userId"
);
userIdTextField.setRequired
(
true
);
add
(
userIdTextField);
...
Si nous relançons les tests, ils devraient tous passer :
III-C-3-e. Remaniement des tests et de l'implémentation▲
A présent nous pouvons prendre un peu de temps pour effectuer quelques remaniements dans le code en utilisant le mécanisme d'héritage des Pages de Wicket :
Les fichiers LoggedInPage.html et HomePage.html sont très similaires finalement, la structure commune pourrait être la suivante :
<html>
<head>
<title>MyNotes</title>
</head>
<body>
<center>
<table cellpadding
=
"0"
cellspacing
=
"0"
>
<tr>
<td>
//contenu
</td>
</tr>
</table>
</center>
</body>
</html>
Par ailleurs le feebackPanel sera utilisé par toutes les pages lors de la remontée d'erreur. Ce composant doit remonter aussi une page de base.
Enfin, dès lors que nous avons dû récupérer la session de la page, nous avons dû transtyper en MyNotesSession. Cela alourdit l'écriture et la lecture du code. Nous pouvons donc écrire une méthode getMyNoteSession() dans cette page de base. Il en résulte de l'écriture de la classe MyNotesPage et son fichier html :
public
class
MyNotesPage extends
WebPage {
public
MyNotesPage
(
) {
add
(
new
FeedbackPanel
(
"feedback"
));
}
protected
MyNotesSession getMyNotesSession
(
) {
return
(
MyNotesSession) getSession
(
);
}
}
<html>
<head>
<title>MyNotes</title>
</head>
<body>
<center>
<table cellpadding
=
"0"
cellspacing
=
"0"
>
<tr>
<td>
<span wicket
:
id
=
"feedback"
/>
</td>
</tr>
<tr>
<td>
<div id
=
"content"
>
<wicket:
child/>
</div>
</td>
</tr>
</table>
</center>
</body>
</html>
La balise <wicket:child/> indique à Wicket que toutes les pages qui dériveront de cette page produiront un code html dans le cadre <div id="content">. En conséquence, les pages HomePage et LoggedInPage vont dériver de MyNotesPage. Les fichiers html correspondant auront comme balise racine <wicket:extend> :
public
class
HomePage extends
MyNotesPage {
public
HomePage
(
) {
add
(
new
LoginForm
(
"loginForm"
));
//suppression de l'ajout du feedbackPanel
}
public
class
LoginForm extends
Form {
...
public
LoginForm
(
String id) {
...
Button button =
new
Button
(
"submit"
) {
@Override
public
void
onSubmit
(
) {
User user;
try
{
user =
securityService.authenticate
(
getUserId
(
), getPassword
(
));
if
(
user ==
null
) {
error
(
"Identifiant ou mot de passe invalide."
);
}
else
{
MyNotesSession session =
getMyNotesSession
(
);
session.setUser
(
user);
setResponsePage
(
LoggedInPage.class
);
}
}
catch
(
Exception e) {
error
(
e.getMessage
(
));
}
}
}
;
add
(
button);
}
...
}
<wicket:
extend>
<form wicket
:
id
=
"loginForm"
>
<table>
<th>
<td><span wicket
:
id
=
"loginTitle"
>
Connexion</span></td>
</th>
<tr>
<td>
Identifiant :
</td>
<td>
<input wicket
:
id
=
"userId"
name
=
"userId"
type
=
"text"
size
=
"50"
/>
</td>
</tr>
<tr>
<td>
Mot de passe :
</td>
<td>
<input wicket
:
id
=
"password"
name
=
"password"
type
=
"password"
size
=
"10"
/>
</td>
</tr>
<tr>
<td></td>
<td>
<input wicket
:
id
=
"submit"
type
=
"submit"
value
=
"Connexion"
>
</td>
</tr>
</table>
</form>
</wicket
:
extend>
public
class
LoggedInPage extends
MyNotesPage {
public
LoggedInPage
(
) {
add
(
new
LogoutLink
(
));
User user =
getMyNotesSession
(
).getUser
(
);
add
(
new
Label
(
"user"
, user.getFullName
(
)));
}
...
}
<wicket:
extend>
<table cellpadding
=
"0"
cellspacing
=
"0"
>
<tr>
<td>
<a wicket
:
id
=
"logoutLink"
name
=
"logoutLink"
><span wicket
:
id
=
"label"
>
logout</span></a>
</td>
<td>
Bonjour <span wicket
:
id
=
"user"
></span>
et bienvenue dans l'application MyNotes.
</td>
</tr>
</table>
</wicket
:
extend>
Ainsi, le code gagne en lisibilité et robustesse car nous avons supprimé la duplication du code html. En relançant les tests, nous constatons qu'il n'y a pas eu de régression :
III-D. Conclusion▲
Nous venons de finir l'implémentation de notre premier besoin et nous constatons que l'effort fourni a été assez conséquent. Ceci est normal car nous avons posé les bases de notre application en appréhendant et intégrant les frameworks applicatifs et de test. Nous verrons que lorsque nous exécuterons le scénario 4, le développement s'accélérera de manière significative. En effet, le chapitre suivant traitera de la création du cycle complet de construction de l'application en introduisant l'exécution des tests fonctionnels.