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. Écriture 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 :
- Écriture du test qui doit correspondre à un besoin concret
- Exécution du test qui doit échouer, car l'implémentation n'est pas encore faite
- Écriture de l'implémentation
- Exécution du test qui doit passer
- Remaniement éventuel du test et de l'implémentation
III-A-1. Écriture 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.
À 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 de 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 - Écriture 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. Écriture 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;
    }
}À 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 : Écriture de la couche Web▲
À 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 eue 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 :
- Écriture 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. Écriture de la page d'accueil▲
III-C-1-a. Écriture 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 sympathiques
- 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. Écriture 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. Écriture 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▲
À 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 correspondants 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.


















