Développement dirigé par les tests : mise en pratique


précédentsommairesuivant

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 :

  1. Ecriture du test qui doit correspondre à un besoin concret
  2. Exécution du test qui doit échouer car l'implémentation n'est pas encore faite
  3. Ecriture de l'implémentation
  4. Exécution du test qui doit passer
  5. 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)
Classe de test SecurityDaoTest.java
Sélectionnez

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 ;) :

JavaBean User.java
Sélectionnez

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é */
    ...
}
Interface SecurityDao.java
Sélectionnez

public interface SecurityDao {
    public User authenticate(String userId, String password);
}
Implémentation SecurityDaoImpl.java
Sélectionnez

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

Test en échec
Test en échec

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 :

Fichier de données DBUnit SecurityDaoTest.xml
Sélectionnez
<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) :

SecurityDaoTest.java
Sélectionnez

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 :

SecurityDaoTest.java
Sélectionnez

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 :

Echec du test
Echec du test

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 :

Fichier SQlMap Security.xml
Sélectionnez
<?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 :

Fichier de configuration iBatis sqlMapconfig.xml
Sélectionnez
<?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 :

Fichier de propriétés de la connexion JDBC database.properties
Sélectionnez

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) :

SecurityDaoImpl.java
Sélectionnez

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 :

Exécution du test réussie

Est-ce que tous les cas sont couverts ? Non, il manque le cas où l'utilisateur n'existe pas. Rajoutons quelques tests :

SecurityDaoTest.java
Sélectionnez

...
@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 :

Exécution de la classe de test réussie

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 :

pom.xml
Sélectionnez
...
<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 :

applicationContext-dao.xml
Sélectionnez
<?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 :

SecurityDaoTest.java
Sélectionnez

@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 :

applicationContext-dao-test.xml
Sélectionnez
<?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 :

Exécution de la classe de test réussie
Exécution de la classe de test réussie

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.

Classe de test AbstractDaoTestCase.java
Sélectionnez

@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 :

SecurityDaoTest.java
Sélectionnez

@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 :

applicationContext-dao.xml
Sélectionnez
<?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 :

sqlMapConfig.xml
Sélectionnez
<?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 :

SecurityDaoImpl.java
Sélectionnez

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 :

SecurityDaoImpl.java (sans spring-orm)
Sélectionnez

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 :

Exécution de la classe de test réussie

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 :

Classe de test SecurityServiceTest.java
Sélectionnez

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 :

Interface SecurityService.java
Sélectionnez

public interface SecurityService {
    public User authenticate(String userId, String password) throws Exception;
}
Implémentation SecurityServiceImpl.java
Sélectionnez

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

Test authenticateSucess en échec
Test authenticateSucess en échec

III-B-3. Implémentation

Ce sera très simple. Il suffit d'invoquer la méthode authenticate(String, String) de SecurityDao :

SecurityServiceImpl.java
Sélectionnez

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" ;) :

SecurityDaoMock.java
Sélectionnez

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 :

SecurityDaoMock.java
Sélectionnez

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 :

SecurityServiceImpl.java
Sélectionnez

public class SecurityServiceImpl implements SecurityService {

    ...

    public void setSecurityDao(SecurityDao securityDao) {
        this.securityDao = securityDao;
    }
}

Dans notre test, settons le mock :

SecurityServiceTest.java
Sélectionnez

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 :

./images/testServiceAuthentificateSuccessOk.jpg
Test authenticate passe

Nous pouvons rajouter les tests d'échec et les exécuter :

SecurityServiceTest.java
Sélectionnez

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);
    }
}
Exécution de la classe de test

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 :

applicationContext-dao-test.xml
Sélectionnez

<?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 :

applicationContext-business.xml
Sélectionnez
<?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 :

SecurityServiceTest.java
Sélectionnez

@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 :

Exécution de la classe de test SecurityServiceTest

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 :

pom.xml
Sélectionnez

...
 <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 :

SecurityServiceTest.java
Sélectionnez

@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 :

pom.xml
Sélectionnez

...
<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.

Page 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 :

HomePageTest.java
Sélectionnez

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
MyNotesApplication.java
Sélectionnez

public class MyNotesApplication extends WebApplication {

    @Override
    public Class getHomePage() {
        return HomePage.class;
    }
}
HomePage.java
Sélectionnez

public class HomePage extends WebPage {
}
HomePage.html
Sélectionnez

<html>
<head>
    <title>MyNotes</title>
</head>

<body>
TODO
</body>
</html>

III-C-1-b. Exécution du test qui doit échouer

Test de rendu de HomePage
Test de rendu de HomePage

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.

HomePage.java
Sélectionnez

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 :

HomePage.html
Sélectionnez

<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

Exécution du test de rendu de HomePage réussie
Exécution du test de rendu de HomePage réussie

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.

Page de connexion

III-C-2-a. Ecriture du test

Nous allons créer un nouveau test pour une page nommée LoggedInPage :

LoggedInPageTest.java
Sélectionnez

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

Exécution du test de rendu de LoogedInPage
Exécution du test de rendu de LoogedInPage

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 :

HomePage.java
Sélectionnez

...
@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());
        }
    }
};
...

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 :

MyNotesSession.java
Sélectionnez

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 :

MyNotesApplication.java
Sélectionnez

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 :

HomePageTest.java
Sélectionnez

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);
    }
    ...
}
HomePage.java
Sélectionnez

public class HomePage extends WebPage {

    public HomePage() {
        add(new LoginForm("loginForm"));
        add(new FeedbackPanel("feedback"));
    }
    ...
}
HomePage.html
Sélectionnez

<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 :

LoggedInPageTest.java
Sélectionnez

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.
UserFactory.java
Sélectionnez

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

Exécution du test de rendu de LoggedInPage
Exécution du test de rendu de LoggedInPage

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

LoggedInPageTest
Sélectionnez

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

Exécution du test de rendu de LoggedInPage
Exécution du test de rendu de LoggedInPage

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.
LoggedInPage.java
Sélectionnez

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());
        }
    }
}
LoggedInPage.html
Sélectionnez

<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

Exécution du test de rendu de LoggedInPage
Exécution du test de rendu de LoggedInPage

Nous pouvons rajouter un test d'erreur de connexion :

LoggedInPageTest.java
Sélectionnez

...
@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 :

Image non disponible

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 :

HomePage.java
Sélectionnez

 ...
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 :

Image non disponible

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 :

 
Sélectionnez

<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 :

MyNotesPage.java
Sélectionnez

public class MyNotesPage extends WebPage {

    public MyNotesPage() {
        add(new FeedbackPanel("feedback"));
    }


    protected MyNotesSession getMyNotesSession() {
        return (MyNotesSession) getSession();
    }
}
MyNotesPage.html
Sélectionnez

<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> :

HomePage.java
Sélectionnez

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);
        }
    ...
}
HomePage.html
Sélectionnez

<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>
LoggedInPage.java
Sélectionnez

public class LoggedInPage extends MyNotesPage {

    public LoggedInPage() {
        add(new LogoutLink());
        User user = getMyNotesSession().getUser();
        add(new Label("user", user.getFullName()));
    }
    ...
}
LoggedInPage.html
Sélectionnez

<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 :

Image non disponible

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.

III-E. Téléchargement

Les sources du projet de ce chapitre sont disponibles via ftpftp ou bien via httphttp. Le jar iBatis prêt à être déposé dans le dépôt Maven est disponible via ftpftp ou bien via httphttp.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2009 David Boissier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.