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


précédentsommairesuivant

V. Chapitre 4 - Gestion des notes

Dans ce chapitre, nous allons donner à l'utilisateur la possibilité d'ajouter, de modifier et de supprimer ses notes. Nous rappelons qu'une note est définie par un titre et un contenu et qu'elle n'appartient qu'à un seul utilisateur.

Image non disponible
Modèle de données

Ci-dessous le script de création de table :

 
Sélectionnez

CREATE TABLE NOTE (
    ID         INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) NOT NULL,
    USER_ID    VARCHAR(20)  NOT NULL,
    TITLE      VARCHAR(255) NULL,
    CONTENT    LONGVARCHAR  NULL,
    PRIMARY KEY(ID),
    FOREIGN KEY (USER_ID) REFERENCES UTILISATEUR (USER_ID)
);

Nous allons commencer ce scénario par l'écriture du test fonctionnel avec la spécification sommaire suivante :

  • Une fois connecté, l'utilisateur accèdera à la gestion de ses notes au travers d'un hyperlien Notes,
  • Un hyperlien Ajouter sera disponible et permettra à l'utilisateur d'ajouter une nouvelle note au travers d'un écran spécifique,
  • Un tableau listant toutes ses notes sera affiché et les opérations Supprimer et de Modifier seront accessibles pour chaque note. La première opération aura pour effet de supprimer en base la note sélectionnée. La deuxième permettra de modifier la note sélectionnée au travers d'un écran similaire à celui de l'opération Ajouter.

V-A. Ecriture du test fonctionnel

Avant d'attaquer le vif du sujet, nous allons introduire un petit formalisme afin d'améliorer la lisibilité. Un test fonctionnel sera décomposé en 3 fichiers :

  • MyTest.input : fichier DBUnit de données de test en entrée en mode Flatten,
  • MyTest.xml : fichier Webtest du test fonctionnel,
  • MyTest.output : fichier DBUnit de données étalon (après l'exécution du test web) en mode Flatten.

Les balises AssertData et InsertData seront déclarées dans un fichier MyProject.dtd afin de ne pas avoir à les inclure dans chaque test.

MyWebTest.dtd
Sélectionnez

<?xml version="1.0" encoding="UTF-8" ?>

<!ENTITY insertData__xml SYSTEM "../includes/InsertData.xml">
<!ENTITY assertData__xml SYSTEM "../includes/AssertData.xml">

Le fichier modèle webtest sera défini de la manière suivante :

MyTest.xml
Sélectionnez

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE project SYSTEM "../config/MyWebTest.dtd">

<project default="test">

    <property name="inputDataFile" value="usecase/MyTest.input"/>
    <property name="outputDataFile" value="usecase/MyTest.output"/>

    <target name="test">
        &insertData__xml;

        <webtest name="Test something">
            ...//test steps are here
        </webtest>

        &assertData__xml;
    </target>
</project>

Appliquons ce formalisme dans l'écriture du test de gestion des notes :

Intitulé Contenu Fichier résultant
Données en entrée [Note 0] Supermarché : Beurre, farine, lait
[Note 1] Ordinateur : Remplacer carte graphique
NotesManagement.input
Etapes du test webtest 1 - Se connecter à l'application,
2 - Asserter la liste des notes de l'utilisateur,
3 - Ajouter une note (Administration : Payer le premier tiers provisionnel),
4 - Asserter la liste des notes de l'utilisateur,
5 - Supprimer la note "Ordinateur",
6 - Asserter la liste des notes,
7 - Modifier la note 0 (Supermarché : Whisky, Vodka),
8 - Asserter la liste des notes.
NotesManagement.xml
Données en sortie [Note 0] Supermarché : Whisky, Vodka
[Note 2] Administration : Payer le premier tiers provisionnel
NotesManagement.output



Concernant le fichier webtest, nous remarquons que l'assertion du contenu d'une cellule n'est pas très lisible.

 
Sélectionnez

<verifyText description="verify note title" text="Supermarché">
    <table row="1" column="1" htmlid="noteList"/>
</verifyText>

Sous la balise verifyText, il faut préciser la table et les coordonnées de la cellule. Si nous devons asserter le contenu d'une table de 10 lignes par 10 colonnes, nous risquons de souffrir quelque peu :(.

Nous essayerons d'améliorer cela en fin de chapitre grâce à un petit développement.

V-B. Implémentation du besoin

Le besoin fonctionnel de ce scénario étant simple (opérations "CRUD" principalement), nous nous focaliserons sur l'écriture et l'exécution des tests afin de ne pas alourdir la lecture de ce tutoriel. Lorsque certains passages demanderont davantage de détails, des fragments du code de production seront présentés.

V-B-1. Ecriture de la couche Dao

V-B-1-a. Ecriture du test de sélection des notes d'un utilisateur

Seules les opérations de sélection des notes d'un utilisateur, de sélection d'une note et d'insertion d'une note seront présentées.

Nous allons pouvoir réutiliser la classe AbstractDaoTestCase extraite dans le scénario 2 et le formalisme DBUnit introduit dans l'écriture du test fonctionnel sera également appliqué dans la couche DAO (format Flatten .input pour les données en entrée et .output pour les données en sortie).

Notre premier test sélectionnera les notes de l'utilisateur tylerdurden avec les données suivantes en entrée :

NoteDaoTest.input
Sélectionnez

<dataset>
    <UTILISATEUR USER_ID="johndoe" FIRST_NAME="John" LAST_NAME="Doe"
                 PASSWORD="seven" EMAIL="john.doe@abc.com" IS_ADMIN="FALSE"/>
    <UTILISATEUR USER_ID="tylerdurden" FIRST_NAME="Tyler" LAST_NAME="Durden"
                 PASSWORD="soap" EMAIL="tyler.durden@abc.com" IS_ADMIN="FALSE"/>

    <NOTE ID="0" USER_ID="johndoe" TITLE="Livraison" CONTENT="Envoyer paquet à l'inspecteur Somerset"/>
    <NOTE ID="1" USER_ID="tylerdurden" TITLE="Savon" CONTENT="Vendre tout le stock "/>
    <NOTE ID="2" USER_ID="tylerdurden" TITLE="Fight Club" CONTENT="Recruter à Chicago"/>
</dataset>

Afin de pouvoir charger notre fichier DBunit en entrée, nous allons remanier la classe AbstractDaoTestCase en introduisant la méthode abstraite getDataSetFile().

AbstractDaoTestCase.java
Sélectionnez

    ...
    private FlatXmlDataSet getDataSet() throws Exception {
        return new FlatXmlDataSet(getClass().getResourceAsStream(getDataSetFile()));
    }
    ...

    protected abstract String getDataSetFile();
    ...

Dans notre test, nous asserterons que le contenu de la liste de notes retournée par la méthode selectAllByUser() :

NoteDaoTest.java
Sélectionnez

@ContextConfiguration(locations = {"applicationContext-dao.xml"})
public class NoteDaoTest extends AbstractDaoTestCase {

    @Autowired
    private NoteDao noteDao;


    @Test
    public void selectAllByUser() throws Exception {
        List<Note> noteList = noteDao.selectAllByUser("tylerdurden");
        assertThat(noteList, equalTo(buildNoteList(new Object[][]{
              {1, "tylerdurden", "Savon", "Vendre tout le stock"},
              {2, "tylerdurden", "Fight Club", "Recruter à Chicago"}
        })));
    }


    @Override
    protected String getDataSetFile() {
        return "NoteDaoTest.input";
    }

    ...
}

Nous représentons les données étalon sous la forme d'un tableau afin de garder une lecture simple et immédiate du test.

Nous introduisons dans ce test la méthode assertThat de JUnit 4 qui permet d'utiliser des Matchers (assertion par contrat, voir ce tutoriel) et donc de tendre vers une écriture proche du langage naturel. Dans notre cas nous avons utilisé le matcher IsEqual.equalTo pour comparer la liste des notes retournée et celle attendue. La méthode buildNoteList() utilisée dans l'assertion construira une liste de notes à partir d'un tableau de données :

NoteDaoTest.java
Sélectionnez

    ...
    private static List<Note> buildNoteList(Object[][] datas) throws Exception {
        String[] columnNames = new String[]{"id", "userId", "title", "content"};
        List<Note> noteList = new ArrayList<Note>();
        for (Object[] data : datas) {
            Note note = new Note();
            for (int j = 0; j < columnNames.length; j++) {
                PropertyUtils.setSimpleProperty(note, columnNames[j], data[j]);
            }
            noteList.add(note);
        }
        return noteList;
    }
    ...

V-B-1-b. Ecriture du test de sélection d'une note

Nous utiliserons les mêmes données en entrée que le test de selectAllByUser() :

NoteDaoTest.java
Sélectionnez

@Test
public void selectById() throws Exception {
    Note note = noteDao.selectById(1);
    assertThat(note, equalTo(buildNote(
          new Object[]{1, "tylerdurden", "Savon", "Vendre tout le stock})));
}

La méthode buildNote() créera un objet Note avec un tableau de valeurs.

NoteDaoTest.java
Sélectionnez

    ...
    private static Note buildNote(Object[] data) throws Exception {
        String[] columnNames = new String[]{"id", "userId", "title", "content"};
        Note note = new Note();
        for (int j = 0; j < columnNames.length; j++) {
            PropertyUtils.setSimpleProperty(note, columnNames[j], data[j]);
        }
        return note;
    }

V-B-1-c. Remaniement de la classe de test

Nous pouvons remanier le test en réutilisant la méthode buildNote(Object[]) dans la méthode buildNoteList(Object[][]). Ensuite, la description des colonnes peut être mise en commun au travers d'une constante.

NoteDaoTest.java
Sélectionnez

    ....
    private static final String[] NOTE_COLUMN_NAMES = new String[]{"id", "userId", "title", "content"};
    ...
    private static List<Note> buildNoteList(Object[][] datas) throws Exception {
        List<Note> noteList = new ArrayList<Note>();
        for (Object[] data : datas) {
            Note note = buildNote(data);
            noteList.add(note);
        }
        return noteList;
    }


    private static Note buildNote(Object[] data) throws Exception {
        Note note = new Note();
        for (int j = 0; j < NOTE_COLUMN_NAMES.length; j++) {
            PropertyUtils.setSimpleProperty(note, NOTE_COLUMN_NAMES[j], data[j]);
        }
        return note;
    }
    ....

V-B-1-d. Ecriture du test d'insertion d'une note

Dans ce test, nous utiliserons les mêmes données en entrée que celles des tests précédemment écrits et nous assertons que la note Java : Refactorer classe Beer a été ajoutée en base.

NoteDaoTest.java
Sélectionnez

@Test
public void insertNote() throws Exception {
    noteDao.insert(buildNote(new String[]{null, "johndoe", "Java", "refactorer classe Beer"}));
    assertTableOutput("NoteDaoTest_insertNote.output", "NOTE");
}
NoteDaoTest_insertNote.output
Sélectionnez

<dataset>
    <NOTE ID="0" USER_ID="johndoe" TITLE="Livraison" CONTENT="Envoyer paquet à l'inspecteur Somerset"/>
    <NOTE ID="1" USER_ID="tylerdurden" TITLE="Savon" CONTENT="Vendre tout le stock"/>
    <NOTE ID="2" USER_ID="tylerdurden" TITLE="Fight Club" CONTENT="Recruter à Chicago"/>
    <NOTE ID="3" USER_ID="johndoe" TITLE="Java" CONTENT="refactorer classe Beer"/>
</dataset>

La méthode assertTableOutput consistera à comparer les données étalon d'une table au format DBUnit et les données en base. Nous aurons besoin de :

  • Récupérer la connexion DBUnit,
  • Construire le dataset avec le fichier étalon,
  • Comparer les données de la table avec le dataset précédemment construit.

Tout d'abord, dans la classe abstraite AbstractDaoTestCase, la méthode getDataset() va changer de signature : elle sera visiblité protected, aura en paramètre le nom du fichier DBUnit et sera renommée buildDataSet(). En conséquence la méthode initDataSet() qui l'utilise sera impactée. Ensuite la connexion dbUnitConnection sera accessible aux classes concrètes, initialisée dans la méthode setUp() puis fermée dans la méthode tearDown().

AbstractDaoTestCase.java
Sélectionnez

public abstract class AbstractDaoTestCase {

    @Autowired
    private IDatabaseTester databaseTester;
    protected IDatabaseConnection dbUnitConnection;


    @Before
    public void setUp() throws Exception {
        dbUnitConnection = databaseTester.getConnection();

        initDatabase();
        initDataSet();
    }
    
    @After
    public void tearDown() throws Exception {
        dbUnitConnection.close();
    }
    ...
    private void initDataSet() throws Exception {
        ...
        DatabaseOperation.CLEAN_INSERT.execute(dbUnitConnection, buildDataSet(getDataSetFile()));
    }


    protected FlatXmlDataSet buildDataSet(String dataSetFile) throws Exception {
        return new FlatXmlDataSet(getClass().getResourceAsStream(dataSetFile));
    }

   ...
}

La méthode assertTableOutput sera accessible depuis la classe AbstractDaoTestCase :

AbstractDaoTestCase.java
Sélectionnez

    ...
    protected void assertTableOutput(String dataSetOutputFilename,
                                   String tableName) throws Exception {
        ITable actualNoteTable = dbUnitConnection.createDataSet().getTable(tableName);
        ITable expectedNoteTable = buildDataSet(dataSetOutputFilename).getTable(tableName);
        Assertion.assertEquals(expectedNoteTable, actualNoteTable);
    }
    ...

La méthode Assertion.assertEquals utilisée ci-dessus est issue de la classe org.dbunit.Assertion (ne pas se tromper avec celle de JUnit).

V-B-2. Ecriture de la couche Service

Dans cette section, nous nous focaliserons essentiellement sur l'utilisation de Mockito lors de l'écriture des tests.

L'interface de Mockito a bien évolué. Auparavant, l'enchaînement de méthodes utilisé était stub...toReturn. Depuis la version 1.6, il est conseillé d'utiliser when...thenReturn (consulter la javadoc).

Cette façon de nommer les méthodes rappelle fortement un article de Martin Fowler sur les Fluent Interfaces, dans lequel il explique que la combinaison du pattern Builder et de signature des méthodes chaînées qui se rapprochent du langage naturel permet d'améliorer la lisibilité du code. Ainsi dans Mockito, when(mock.aMethod()).thenReturn(Something) paraît plus lisible et plus naturel que stub(mock.aMethod()).toReturn(Something). Nous retrouvons également ce type de démarche au travers des Matchers utilisés dans JUnit 4 (assertThat(a, isGreaterThan(b)).

A l'identique du chapitre précédent, seule une partie des tests unitaires de la couche seront développés. Le reste sera disponible via les sources du projet.

V-B-2-a. Ecriture du test de sélection des notes de l'utilisateur

Afin que notre test NoteServiceTest puisse bénéficier des méthodes buildNoteList() et buildNote() issues de NoteDaoTest, nous allons créer une classe utilitaire de test pour les tests liés aux notes et nous la mettrons dans le package model :

NoteTestUtil.java
Sélectionnez

public class NoteTestUtil {
    public static final String[] NOTE_COLUMN_NAMES = new String[]{"id", "userId", "title", "content"};


    private NoteTestUtil() {}


    public static List<Note> buildNoteList(Object[][] datas) throws Exception {...}


    public static Note buildNote (Object[] data) throws Exception {...}
}

Notre test se décomposera de la façon suivante :

  1. Initialisation de NoteService et de NoteDao,
  2. Simulation de l'appel de la méthode selectAllByUser(),
  3. Assertion du résultat escompté,
  4. Assertion du nombre d'appels de la méthode selectAllByUser() de NoteDao.
NoteServiceTest.java
Sélectionnez

public class NoteServiceTest {

    @Autowired
    private NoteService noteService;

    @Mock
    private NoteDao noteDaoMock;


    @Test
    public void selectAllByUser() throws Exception {
        when(noteDaoMock.selectAllByUser("tylerdurden")).thenReturn(
          buildNoteList(new Object[][]{
                {1, "tylerdurden", "Savon", "Aller chercher de la graisse dans les centres de liposucion"},
                {2, "tylerdurden", "Fight Club", "Recruter à Chicago"}
          }));

        List<Note> noteList = noteService.selectAllByUser("tylerdurden");
        assertThat(noteList, equalTo(new Object[][]{
          {1, "tylerdurden", "Savon", "Aller chercher de la graisse dans les centres de liposucion"},
          {2, "tylerdurden", "Fight Club", "Recruter à Chicago"}
    }));

        verify(noteDaoMock, times(1)).selectAllByUser("tylerdurden");
    }
    
    
    @Before
    public void setUp() throws Exception{
        MockitoAnnotations.initMocks(this);
        ReflectionTestUtils.setField(noteService, "noteDao", noteDaoMock);
    }
}

Nous pouvons nous poser la question sur l'utilité de renvoyer la même liste de notes que celle attendue. Nous aurions pu déclarer une liste de note en tant que variable locale et asserter que le résultat était bien identique.

Autre manière d'écrire le test
Sélectionnez

@Test
public void selectAllByUser() throws Exception {
    List<Note> expectedNoteList = buildNoteList(new Object[][]{
                {1, "tylerdurden", "Savon", "Aller chercher de la graisse dans les centres de liposucion"},
                {2, "tylerdurden", "Fight Club", "Recruter à Chicago"}
          })
        when(noteDaoMock.selectAllByUser("johndoe")).thenReturn(expectedNoteList);

    List<Note> noteList = noteService.selectAllByUser("tylerdurden");
    assertThat(noteList, equalTo(expectedNoteList));

    verify(noteDaoMock, times(1)).selectAllByUser("tylerdurden");
}

A la lecture de ce test, nous avons l'impression que l'assertion et la méthode simulée partagent les mêmes données ce qui peut induire des erreurs d'interprétation (existe-il une dépendance de données entre l'assertion et la méthode simulée ?).
Bien que l'implémentation actuelle de NoteService ne fasse rien d'autre que d'appeler les méthodes de la couche Dao, si un besoin fonctionnel devait apparaître au cours du projet (exemple : convertir le titre de la note en majuscule), nous ne pourrions plus appliquer cela.
Il est donc nécessaire de séparer les données en entrée des données de sortie.

V-B-2-b. Ecriture du test d'insertion d'une note

L'écriture est très similaire à ce que nous avons écrit précédemment.

NoteServiceTest.java
Sélectionnez

@Test
public void insertNote() throws Exception {
    when(noteDaoMock.insert(any(Note.class))).thenAnswer(new Answer<Note>() {

        public Note answer(InvocationOnMock invocationOnMock) throws Throwable {
            Object[] arguments = invocationOnMock.getArguments();
            final Note note = (Note)arguments[0];
            note.setId(1);
            return note;
        }
    });

    Note noteInserted = noteService.insertNote(buildNote(
          new Object[]{10, "deckard", "Travail", "Rapport d'activité"}));

    assertThat(noteInserted, notNullValue());
    
    assertThat(noteInserted.getId(), equalTo(1));

    verify(noteDaoMock, times(1)).insert(any(Note.class));
}
  • L'utilisation de la méthode any(Note.class) permet d'indiquer à Mockito d'intercepter l'appel de la méthode insert pour n'importe quel instance de la classe Note, ce qui est très pratique dans notre cas de test.
  • L'utilisation de la méthode thenAnswer(Answer) permet au développeur de contrôler les évènements qui surviennent lors de l'appel de la méthode simulée. Dans notre cas, nous nous contentons de renvoyer le bean Note avec un identifiant de note "codé en dur".

En conclusion de cette section, l'API Mockito nous simplifie l'écriture des tests et l'utilisation d'interfaces qui suivent la philosophie Fluent Interfaces permet d'améliorer la lisibilité des tests.

V-B-3. Ecriture de la couche web

Nous allons définir la hiérarchie et la navigation des pages web :

Navigation des pages
Navigation des pages

V-B-3-a. Page principale des notes

V-B-3-a-i. Ecriture du test

Nous allons asserter que la liste de notes et le lien Ajouter sont affichés.

NotePageTest.java
Sélectionnez

public class NotePageTest extends AbstractWicketTestCase {

    @Mock
    private NoteService noteServiceMock;


    @Test
    public void testRender() throws Exception {
        List<Note> noteList = buildNoteList(
              new Object[][]{
                    {0, "johndoe", "Développement", "Java"},
                    {1, "johndoe", "Lean", "accepter l'échec"},
              });

        when(noteServiceMock.selectAllByUser("johndoe")).thenReturn(noteList);

        tester.startPage(NotePage.class);
        tester.assertListView("noteList", noteList);
        tester.assertComponent("insertNoteLink", Link.class);

        verify(noteServiceMock, times(1)).selectAllByUser("johndoe");
    }

    @Override
    protected void doSetUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        applicationContextMock.putBean("noteService", noteServiceMock);
    }
}

Il nous manque donc la création de la classe NotePage qui héritera de la classe LoggedInPage (nous ne pouvons pas accéder à nos notes si nous ne sommes pas connectés).

V-B-3-a-ii. Exécution du test
Exécution du test render de NotePage
Exécution du test render de NotePage

L'erreur indique que la fixture tester n'a pas pu instancier la classe NotePage. Inspectons le code du constructeur LoggedInPage.

LoggedInPage.java
Sélectionnez

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

Dans notre test, nous voyons qu'il manque la phase d'authentification. Dans un environnement de test unitaire, nous aimerions l'éviter afin de ne pas d'alourdir le test.

Ensuite, nous remarquons que le test a mis en évidence un bug. En effet, un accès direct à LoggedInPage lance une exception car getMyNotesSession().getUser() renvoie null et par conséquent l'appel de user.getFullName() génère une exception de type NullPointerException.

Enfin, LoggedInPage n'autorise pas d'héritage de page (pas de balise <wicket:child/>), nous devons la rendre abstraite afin que NotePage puisse en dériver.

En résumé, nous avons 3 tâches :

  • Dans LoggedInPage, rediriger l'utilisateur vers HomePage si jamais il ne s'est pas authentifié,
  • Transformer LoggedInPage en classe abstraite. Une classe WelcomePage en dérivera ainsi que la classe NotePage,
  • Pouvoir simuler la phase d'authentification de manière simple dans les tests unitaires.
V-B-3-a-iii. Remaniement de l'existant

Nous rajouterons la vérification de l'authentification utilisateur dans le constructeur de la classe LoggedInPage. Dans le mauvais cas, l'utilisateur sera redirigé vers la page de connexion.

LoggedInPage.java
Sélectionnez

public abstract class LoggedInPage extends MyNotesPage {

    protected LoggedInPage() {
        if (!getMyNotesSession().isAuthenticated()) {
            throw new RestartResponseAtInterceptPageException(HomePage.class);
        }
        add(new LogoutLink());
    }


    private class LogoutLink extends Link {...}
}
LoggedInPage.html
Sélectionnez

<wicket:extend>
    <div id="xo_alignement">
        <!-- Menu -->
        <div id="xo_menu">
            <ul id="menuitems">
                <li><a wicket:id="noteLink" id="noteLink"><img src="/mynotes/images/note.png"/>Notes</a></li>
            </ul>
        </div>

        <!-- Contenu -->
        <wicket:child/>
    </div>

</wicket:extend>
							

Le design des pages s'appuie désormais sur du CSS et des DIV.

WelcomePage sera la page d'accueil de Mynotes et donc affichera le message de bienvenue.

WelcomePage.java
Sélectionnez

public class WelcomePage extends LoggedInPage {

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

<wicket:extend>
    <div id="welcomeText">
        Bonjour <span wicket:id="user"/> et bienvenue dans l'application MyNotes.
    </div>
</wicket:extend>

Du coup LoggedInPageTest sera renommé en WelcomePageTest et nous rajouterons un test qui vérifie bien la redirection vers la HomePage dans le cas où l'utilisateur ne s'est pas connecté.

WelcomePageTest.java
Sélectionnez

@Test
public void redirectToHomePageWhenTryingToAccess() throws Exception {
    tester.startPage(WelcomePage.class);
    tester.assertRenderedPage(HomePage.class);
}

Nous allons mettre à disposition au développeur une méthode de simulation de l'authentification de MyNotes dans la classe AbstractWicketTestCase.

AbstractWicketTestCase.java
Sélectionnez

public abstract class AbstractWicketTestCase {

    protected WicketTester tester;

    protected ApplicationContextMock applicationContextMock;


    @Before
    public final void setUp() throws Exception {
        applicationContextMock = new ApplicationContextMock();

        MyNotesApplication webApp = new MyNotesApplication() {
            @Override
            public void init() {
                addComponentInstantiationListener(new SpringComponentInjector(this, applicationContextMock));
            }
        };

        tester = new WicketTester(webApp);

        doSetUp();
    }


    protected abstract void doSetUp() throws Exception;


    protected void mockAuthentification(String userId) {
        MockHttpSession session = tester.getServletSession();
        MyNotesSession myNotesSession = MyNotesSession.session();
        myNotesSession.setUser(UserTestUtil.buildSimpleUser(userId));
        session.setAttribute("wicket:WicketMockServlet:session", myNotesSession);
    }
}

La méthode mockAuthentification permet de stocker dans la session un objet user. C'est un peu "grouillesque" mais cela suffit à combler notre besoin pour l'instant.
Ensuite, nous déplacerons l'appel de la méthode doSetup() afin de pouvoir appeler si besoin mockAuthentification dans notre classe de test.
Enfin, nous renommerons la classe UserFactory en UserTestUtil par souci de cohérence avec la classe NoteTestUtil et nous lui rajouterons la méthode buildSimpleUser().

UserTestUtil.java
Sélectionnez

public class UserTestUtil {

    public static final String[] USER_COLUMN_NAMES =
          new String[]{"userId", "firstName", "lastName", "email", "password", "isAdmin"};


    private UserTestUtil() {
    }


    public static User buildUser(Object[] data) throws Exception {
        User user = new User();
        for (int j = 0; j < USER_COLUMN_NAMES.length; j++) {
            PropertyUtils.setSimpleProperty(user, USER_COLUMN_NAMES[j], data[j]);
        }
        return user;
    }


    public static User buildSimpleUser(String userId) {
        User user = new User();
        user.setUserId(userId);
        return user;
    }
}

Suite à notre remaniement, exécutons tous les tests de la couche web.

Image non disponible

Nous voyons que WelcomePageTest ne passe plus. Après investigation, nous constatons que nous avons oublié de rediriger HomePage vers WelcomePage suite à une authentification valide.

LoginForm.java
Sélectionnez

public class LoginForm extends Form {

    public LoginForm(String id) {
        ...
        Button button = new Button("loginSubmit") {
            @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(WelcomePage.class); //changement de page suite à l'authentification
                    }
                }
                catch (Exception e) {
                    LOG.error(e.getMessage());
                    error(e.getMessage());
                }
            }
        };
    }
    ...
}

Réexécutons les tests :

Execution des tests unitaires de la couche web
Execution des tests unitaires de la couche web

Nous voyons que seul le test NotePage ne passe pas. Nous pouvons enfin finaliser notre test et exécuter la suite de tests de la couche web :

NotePageTest.java
Sélectionnez

    @Test
    public void testRender() throws Exception {

        List<Note> noteList = NoteTestUtil.buildNoteList(
              new Object[][]{
                    {0, "johndoe", "Développement", "Java"},
                    {1, "johndoe", "Lean", "accepter l'échec"},
              });

        when(noteServiceMock.selectAllByUser("johndoe")).thenReturn(noteList);


        tester.startPage(NotePage.class);
        tester.assertListView("noteList", buildNoteList(
              new Object[][]{
                    {0, "johndoe", "Développement", "Java"},
                    {1, "johndoe", "Lean", "accepter l'échec"},
              }));
        tester.assertComponent("insertNoteLink", Link.class);

        verify(noteServiceMock, times(1)).selectAllByUser("johndoe");
    }


    @Override
    protected void doSetUp() throws Exception {
        noteServiceMock = Mockito.mock(NoteService.class);
        applicationContextMock.putBean("noteService", noteServiceMock);

        mockAuthentification();
    }
Image non disponible

Nous venons de constater l'importance des tests surtout lorsque des tâches de remaniement sont effectuées. L'application est minuscule, il faut imaginer l'impact de quelques modifications dans une application de plus grande envergure. L'exécution des tests nous donne un résultat immédiat sur les éventuelles régressions et nous évite certaines tâches de correction fastidieuses suite à un bug signalé en recette voire en production.

V-B-3-a-iv. Implémentation
NotePage.java
Sélectionnez

public class NotePage extends LoggedInPage {

    @SpringBean
    private NoteService noteService;


    public NotePage() {
        try {
            add(new NoteListView("noteList", noteService.selectAllByUser(getUser().getUserId())));
            add(new InsertNoteLink("insertNoteLink"));
        }
        catch (Exception ex) {
            error(ex.getMessage());
        }
    }


    private class NoteListView extends ListView {
        private NoteListView(String id, List<Note> noteList) {
            super(id, noteList);
        }


        @Override
        protected void populateItem(ListItem item) {
            final Note note = (Note)item.getModelObject();
            item.add(new Label("id", note.getId().toString()));
            item.add(new Label("title",
                               note.getTitle()));
            item.add(new Label("content", note.getContent()));
        }
    }

    class InsertNoteLink extends Link {

        private InsertNoteLink(String id) {
            super(id);
        }


        @Override
        public void onClick() {
            try {
//                TODO
            }
            catch (Exception ex) {
                NotePage.this.error(ex);
            }
        }
    }
}
NotePage.html
Sélectionnez

<wicket:extend>
    <h1>Vos notes</h1>

    <div>
        <a href="#" wicket:id="insertNoteLink" id="insertNote">
            <img src="/mynotes/images/note_add.png"/>Ajouter
        </a>
    </div>

    <table id="noteList" border="1">
        <thead>
        <tr>
            <td>#</td>
            <td>Titre</td>
            <td>Contenu</td>
        </tr>
        </thead>
        <tbody>
        <tr wicket:id="noteList">
            <td wicket:id="id">#</td>
            <td wicket:id="title">Titre</td>
            <td wicket:id="content">Contenu</td>
        </tr>
        </tbody>
    </table>
</wicket:extend>

Nous notons que la gestion des listes de données sous la forme de tableau est aisée avec Wicket.

V-B-3-a-v. Ré-exécution du test
Exécution du test testRender de NotePage

A présent, il nous reste la mise à jour et la suppression d'une note (l'insertion ne sera pas présentée).

V-B-3-b. Mise à jour d'une note

D'après le diagramme de navigation des pages, Modifier implique la page NoteEdit. Nous nous contenterons d'asserter que le fait de cliquer sur Modifier amène l'utilisateur sur cette page.

NoteDaoTest
Sélectionnez

    @Test
    public void updateLink() throws Exception {
        List<Note> noteList = NoteTestUtil.buildNoteList(
              new Object[][]{
                    {0, "johndoe", "Développement", "Java"},
                    {1, "johndoe", "Lean", "accepter l'échec"},
              });

        when(noteServiceMock.selectAllByUser("johndoe")).thenReturn(noteList);

        tester.startPage(NotePage.class);
        tester.clickLink("noteList:0:update");
        tester.assertRenderedPage(NoteEditPage.class);

        verify(noteServiceMock, times(1)).selectNoteById(0);
    }

La notation noteList:0:update indique le lien update de la ligne 0 de noteList.

NoteEditPage.java
Sélectionnez

public class NoteEditPage extends LoggedInPage{
}
NoteEdit.html
Sélectionnez

<wicket:extend>
</wicket:extend>
V-B-3-b-i. Exécution du test
Exécution du test de mise à jour d'une note
V-B-3-b-ii. Implémentation
NotePage.java
Sélectionnez

    private class NoteListView extends ListView {
        private NoteListView(String id, List<Note> noteList) {
            super(id, noteList);
        }


        @Override
        protected void populateItem(ListItem item) {
            final Note note = (Note)item.getModelObject();
            ...
            item.add(new UpdateNoteLink("update", note));
        }
    }

    private class UpdateNoteLink extends Link {
        private final Note note;


        private UpdateNoteLink(String id, Note note) {
            super(id);
            add(new AttributeModifier("id", new Model("update_" + note.getId())));
            this.note = note;
        }


        @Override
        public void onClick() {
            try {
                setResponsePage(new NoteEditPage( noteService.selectNoteById(note.getId())));
            }
            catch (Exception ex) {
                NotePage.this.error(ex);
            }
        }
    }
NotePage.html
Sélectionnez

<wicket:extend>
    <h1>Vos notes</h1>

    ...
    
    <table id="noteList" border="1">
        ...
        <tr wicket:id="noteList">
            ...
            <td>
                <a href="#" wicket:id="update" id="updateLinkId" ><img src="/mynotes/images/note_edit.png" alt="Modifier"/></a>
            </td>
        </tr>
        </tbody>
    </table>
</wicket:extend>

Sur l'évènement onClick du lien updateNoteLink, l'utilisateur sera redirigé vers la page NotePageEdit.

Pour chaque note affichée dans la NoteListView, un lien Modifier sera disponible. Lorsque l'utilisateur cliquera sur celui-ci, il sera redirigé vers la page NoteEdit avec les informations de la note à modifier. Nous notons l'utilisation de la classe AttributeModifier qui permet de modifier la valeur d'une propriété html. Dans notre cas, la valeur de l'attribut id de l'hyperlien update aura comme valeur update_[noteId]. Cela permet une bonne lecture du test fonctionnel sans que le code de production soit "altéré".

NoteEditPage
Sélectionnez

public NoteEditPage(Note note) {
//    TODO
}
V-B-3-b-iii. Ré-exécution du test
Image non disponible

V-B-3-c. Suppression d'une note

V-B-3-c-i. Ecriture du test
NotePageTest.java
Sélectionnez

    @Test
    public void deleteNote() throws Exception {
// Stub noteService method invocations
        final List<Note> noteList = buildNoteList(
              new Object[][]{
                    {0, "johndoe", "Développement", "Java"},
                    {1, "johndoe", "Lean", "accepter l'échec"},
              });

        when(noteServiceMock.selectAllByUser("johndoe")).thenReturn(noteList);
        when(noteServiceMock.deleteNote(any(Note.class))).thenAnswer(new Answer<Integer>() {

            public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
                Object[] args = invocationOnMock.getArguments();
                Note deleteNote = (Note)args[0];
                noteList.remove(deleteNote);
                return 1;
            }
        });

// Test steps
        tester.startPage(NotePage.class);
        tester.clickLink("noteList:0:delete");
        tester.assertListView("noteList", buildNoteList(
              new Object[][]{
                    {1, "johndoe", "Lean", "la chasse au muda"},
              }));

// Assert note service method calls
        verify(noteServiceMock, times(1)).deleteNote(Mockito.any(Note.class));
        verify(noteServiceMock, times(2)).selectAllByUser("johndoe");
    }
V-B-3-c-ii. Implémentation
NotePage.java
Sélectionnez

    ...
    private class DeleteNoteLink extends Link {
        private final Note note;


        private DeleteNoteLink(String id, Note note) {
            super(id);
            add(new AttributeModifier("id", new Model("delete_" + note.getId())));
            this.note = note;
        }


        @Override
        public void onClick() {
            try {
                noteService.deleteNote(note);
                setResponsePage(NotePage.class);
            }
            catch (Exception ex) {
                NotePage.this.error(ex);
            }
        }
    }
}
NotePage.html
Sélectionnez

<wicket:extend>
    ...
    <table id="noteList" border="1">
        ...
        <tr wicket:id="noteList">
            ...
            <td>
                <a href="#" wicket:id="update" id="updateLinkId" ><img src="/mynotes/images/note_edit.png" alt="Modifier"/></a>
                <a href="#" wicket:id="delete" id="deleteLinkId"><img src="/mynotes/images/note_delete.png" alt="Supprimer"/></a>
            </td>
        </tr>
        </tbody>
    </table>
</wicket:extend>

A présent il nous reste le lien entre la page principale et la page des notes. Nous rajouterons un lien dans la classe abstraite LoggedInPage et nous mettrons à jour le test WelcomePage

LoggedInPage.java
Sélectionnez

    private static class NoteLink extends Link {

        private NoteLink() {
            super("noteLink");
        }


        @Override
        public void onClick() {
            setResponsePage(NotePage.class);
        }
    }
LoggedInPage.html
Sélectionnez

<wicket:extend>
    <div id="xo_alignement">
        <!-- Menu -->
        <div id="xo_menu">
            <ul id="menuitems">
                <li><a wicket:id="noteLink" id="noteLink"><img src="/mynotes/images/note.png"/>Notes</a></li>
                <li><a wicket:id="logoutLink" id="logoutLink"><img src="/mynotes/images/door_out.png"/>D&eacute;connexion</a></li>
            </ul>
        </div>

        <!-- Contenu -->
        <wicket:child/>
    </div>
</wicket:extend>
WelcomePageTest.java
Sélectionnez

    @Test
    public void testRender() throws Exception {
        tester.startPage(HomePage.class);

        ...

        tester.clickLink("noteLink");
        tester.assertRenderedPage(NotePage.class);
    }

Le test et le code de NoteEditPage ne seront pas présentés car la démarche est similaire. Relançons tous les tests unitaires de l'application :

Exécution des tests unitaires de l'application

A la fin de cette section, nous ressentons une certaine maîtrise dans l'exécution de nos tâches. Les tests nous permettent de remanier le code en toute confiance. Ce dernier prend de la maturité au fur et à mesure. Il vit tout simplement ;).

V-B-4. Exécution des tests fonctionnels

Nous allons rajouter le test des erreurs de connexion dans le test fonctionnel :

ConnexionTest.xml
Sélectionnez

    <webtest name="Test connexion into MyNotes">
        <group description="Bad Login : missing input fields">
            <invoke url="${webapp.home}" description="Go to MyNotes"/>
            <clickButton label="Connexion"/>
        </group>
        <group description="Verify we are still on the login page with the error message">
            <verifyText text="Le champ .*userId.* est obligatoire" regex="true"/>
            <verifyText text="Le champ .*password.* est obligatoire" regex="true"/>
        </group>

        <group description="Bad Login : wrong input fields">
            <setInputField name="userId" value="JohnDoe"/>
            <setInputField name="password" value="six"/>
            <clickButton label="Connexion"/>
        </group>
        <group description="Verify we are still on the login page with the error message">
            <verifyText text="Identifiant ou mot de passe invalide."/>
        </group>

        <group description="Good Login">
            <invoke url="${webapp.home}" description="Go to MyNotes"/>
            <setInputField name="userId" value="JohnDoe"/>
            <setInputField name="password" value="seven"/>
            <clickButton label="Connexion"/>
        </group>
        <group description="Verify welcome text">
            <verifyText text="Bonjour"/>
            <verifyText text="John Doe"/>
            <verifyText text="et bienvenue dans l'application MyNotes."/>
        </group>
        <group description="Disconnect and verify that we are on the login page">
            <clickLink label="Déconnexion"/>
            <verifyText text="Connexion"/>
        </group>
    </webtest>

Dans le deuxième groupe, afin de pouvoir valider le message d'erreur des champs obligatoire, le caractère apostrophe (#039;) semble être mal géré par la balise verifyText. Du coup, nous devons utiliser l'attribut regex et appliquer le pattern .*<nom du champ>.*.

Relançons un build complet avec les tests fonctionnels et vérifions qu'ils passent bien tous.

Rapport d'exécution des tests fonctionnels
Rapport d'exécution des tests fonctionnels

V-C. Allons un peu plus loin ...

Les sections suivantes concernent l'amélioration de la plateforme de tests que nous avons mis en place dans le chapitre 3. Il s'agit de pouvoir exécuter de bout en bout une batterie de tests fonctionnels quel que soit le résultat et d'améliorer l'écriture/lecture de l'assertion des tableaux HTML.

V-C-1. Amélioration de l'exécution des tests fonctionnels

Le processus d'exécution des tests webtest est interrompu dès lors qu'une exception BuildException est levée. Si une assertion échoue lors de l'exécution d'un test i, le suivant ne sera exécuté. Ceci est fâcheux si la suite comporte plus de 200 tests car nous ne pouvons pas mesurer l'impact global du code modifié.

Nous allons créer une tâche Ant WebTestSuite qui, à la manière d'une suite de tests unitaires, lorsqu'un test échouera, journalisera l'erreur soulevée puis passera au test suivant. A la fin de l'exécution, elle génèrera un rapport d'exécution et lancera une exception si au moins un test a échoué.

V-C-1-a. Ecriture du test

Un projet webtest-contrib sera créé à l'occasion et nous développerons sous le package com.webtest.ant.

Après plusieurs essais infructueux sur la simulation de la méthode execute() de la classe Ant, je n'ai pu tester que la méthode qui génèrera la liste des fichiers webtest. Les autres tests concernent la vérification des paramètres de la balise.

WebTestSuiteTaskTest.java
Sélectionnez

public class WebTestSuiteTaskTest extends TestCase {
    
    private WebTestSuiteTask task;


    public void test_buildWebTestListWithBasicTestSuite() throws Exception {
        File webtestdir = new File(getClass().getResource("basictestsuite").toURI());

        task.setWebtestdir(webtestdir);
        List<FileResource> fileList = task.buildWebTestList();
        assertFileList(fileList, new String[]{
              "Test1.xml",
              "Test2.xml",
        });
    }

    private void assertFileList(List<FileResource> actualfileList,
                                String[] expectedFileList) {
        assertEquals(expectedFileList.length, actualfileList.size());

        for (int i = 0; i < expectedFileList.length; i++) {
            String expectedFile = expectedFileList[i];
            FileResource actualFile = actualfileList.get(i);
            assertEquals(expectedFile, actualFile.getName());
        }
    }

    @Override
    public void setUp() {
        Project project = new Project();
        task = new WebTestSuiteTask();
        task.setProject(project);
    }
}

Sachant que les fichiers Test1.xml et Test2.xml sont dans un répertoire nommé basictestsuite :

Image non disponible
WebTestSuiteTask.java
Sélectionnez

public class WebTestSuiteTask extends Task {

    private File webtestdir;

    @Override
    public void execute() throws BuildException {
// TODO
    }

    List<FileResource> buildWebTestList() {
        List<FileResource> webTestFiles = new ArrayList<FileResource>();
        return webTestFiles;
    }


    public void setWebtestdir(File webtestdir) {
        this.webtestdir = webtestdir;
    }
}

V-C-1-b. Exécution du test

Image non disponible

V-C-1-c. Implémentation

L'implémentation sera assez simple. Nous utiliserons un FileSet pour récupérer les fichiers qui nous intéressent (**/*.xml). Puis nous retournerons le résultat sous la forme d'une liste de FileRessource. Par ailleurs, nous journaliserons l'exécution de cette méthode en imprimant le répertoire de tests puis le nombre de tests trouvés.

WebTestSuiteTask.java
Sélectionnez

    ...
    List<FileResource> buildWebTestList() {
        LOG.info("Webtest dir = " + webtestdir.getAbsolutePath());
        FileSet fileSet = new FileSet();
        fileSet.setDir(webtestdir);
        fileSet.setIncludes("**/*.xml");

        fileSet.setProject(getProject());
        List<FileResource> webTestFiles = new ArrayList<FileResource>();
        Iterator resources = fileSet.iterator();
        while (resources.hasNext()) {
            Resource resource = (Resource)resources.next();
            if (!resource.isExists()) {
                continue;
            }

            if (resource instanceof FileResource) {
                FileResource fr = (FileResource)resource;
                webTestFiles.add(fr);
                LOG.debug("test added = '" + fr.getFile().getPath() + "'");
            }
        }
        LOG.info("Found  " + webTestFiles.size() + " webtests");
        return webTestFiles;
    }

Ensuite, nous rendrons paramétrable le pattern d'inclusion puis nous rajouterons la possibilité d'exclure certains fichiers/répertoires.
Enfin, nous donnerons la possibilité de générer le rapport d'exécution des tests avec la feuille XSL de canoo.

Il en résultera les fichier sources suivants : WebTestSuiteTaskTest.java, WebTestSuiteTask.java.

V-C-1-d. Intégration dans le projet mynotes

Installons la petite librairie webtest-contrib dans le dépôt local puis référençons-là dans le profile integration de mynotes.

pom.xml
Sélectionnez

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        ...
        <execution>
            <id>run-webtest</id>
            <phase>integration-test</phase>
            <configuration>
                <tasks>
                    <property name="runtime_classpath" refid="maven.test.classpath"/>
                    ...
                    <ant antfile="${webtest.tests}/allTests.xml" inheritRefs="true"
                         dir="${webtest.tests}" inheritAll="true"/>
                </tasks>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>com.canoo</groupId>
            <artifactId>webtest-contrib</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</plugin>

Ensuite, rajoutons la balise WebTestSuiteTask dans le fichier Config.xml.

Config.xml
Sélectionnez

<taskdef name="webtestsuite" classname="com.canoo.ant.WebTestSuiteTask" classpath="${runtime_classpath}"/>

Enfin, mettons à jour le fichier AllTests.xml.

allTests.xml
Sélectionnez

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE project SYSTEM "config/MyWebTest.dtd">

<project default="runTests">

    &config__xml;

    <target name="runTests" description="runs all the tests">
        <echo message="Starting runnning test suite"/>
        <webtestsuite webtestdir="usecase" reportgeneratorfile="report/generateReport.xml"/>
    </target>
</project>

Si l'on exécute notre suite de tests, tout se passe comme prévu. Il n'y a aucune régression.

Rajoutons une erreur dans un test fonctionnel ConnectionTest.xml.

ConnectionTest.xml
Sélectionnez

    ...
    <group description="Good Login">
                <invoke url="${webapp.home}" description="Go to MyNotes"/>
                <setInputField name="userId" value="johndoe"/>
                <setInputField name="password" value="seven"/>
                <clickButton label="Connexion"/>
    </group>
    <group description="Verify welcome text">
                <verifyText text="Bonjour"/>
                <verifyText text="John Connors"/> <!-- Erreur volontaire -->
                <verifyText text="et bienvenue dans l'application MyNotes."/>
    </group>
    ...

Une erreur a bien été trouvée dans ConnectionTest.xml.

Image non disponible

En ouvrant le rapport généré, nous constatons que le test NotesManagement.xml a été exécuté.

Image non disponible

V-C-2. Amélioration de l'assertion des tableaux HTML

Nous avons vu lors de l'écriture du test fonctionnel de gestion des notes que l'assertion des cellules d'un tableau HTML n'est pas très aisée et pas très lisible :

NotesManagement.xml
Sélectionnez

<group description="Assert that a table is displayed with 2 rows">
    <verifyText description="verify note #" text="0">
        <table row="1" column="0" htmlid="noteList"/>
    </verifyText>
    <verifyText description="verify note title" text="Supermarché">
        <table row="1" column="1" htmlid="noteList"/>
    </verifyText>
    <verifyText description="verify note content" text="Beurre, farine, lait">
        <table row="1" column="2" htmlid="noteList"/>
    </verifyText>

    <verifyText description="verify note #" text="1">
        <table row="2" column="0" htmlid="noteList"/>
    </verifyText>
    <verifyText description="verify note title" text="Ordinateur">
        <table row="2" column="1" htmlid="noteList"/>
    </verifyText>
    <verifyText description="verify note content" text="Remplacer carte graphique">
        <table row="2" column="2" htmlid="noteList"/>
    </verifyText>
</group>

Nous pourrions asserter le contenu avec une représentation ASCII des données :

 
Sélectionnez

<verifyTable htmlid="noteList" excludedColumns="3">
    | # | Titre       | Contenu                   |
    | 0 | Supermarché | Beurre, farine, lait      |
    | 1 | Ordinateur  | Remplacer carte graphique |
</verifyTable>

Si nous appliquons cette représentation sur tout le test cela nous donne une lecture plus compacte et aisée à lire : NotesManagement.xml.

V-C-2-a. Préparatifs

Nous réutiliserons le projet webtest-contrib et nous mettrons notre future balise VerifyTableStep sous com.canoo.webtest.extension.
Nous aurons besoin de classes de test de Canoo et étant donné que le module de tests de webtest n'est pas "mavenifié", il faudra l'importer dans le dépôt local à l'aide de la commande suivante (depuis le répertoire lib de la distribution de canoo) :

 
Sélectionnez

mvn install:install-file -DgroupId=com.canoo.webtest -DartifactId=webtest -Dclassifier=tests -Dversion=R_1689 -Dpackaging=jar -Dfile=webtest_T.jar

V-C-2-b. Ecriture du test

Notre test unitaire étendra la classe com.canoo.webtest.steps.BaseStepTestCase. En entrée, nous aurons un tableau HTML 'tbl' basique à 3 colonnes avec un entête et une ligne de données. Notre premier test sera le cas nominal où l'assertion réussit.

VerifyTableStepTest.java
Sélectionnez

public class VerifyTableStepTest extends BaseStepTestCase {

    private static final String HTML_PAGE_WITH_TABLE =
          "  <html>"
          + "    <head>"
          + "        <title>Test</title>"
          + "    </head>"
          + "    <body>"
          + "        <table id='tbl'>"
          + "            <tr>"
          + "                <th>Denomination</th><th>Prix unitaire</th><th>Qté</th><th>Total</th>"
          + "            </tr>"
          + "            <tr>"
          + "                <td>carotte</td><td>1.00</td><td>5</td><td>5.00</td>"
          + "            </tr>"
          + "        </table>"
          + "    </body>"
          + "</html>";

     public void testOk() throws Exception {
        VerifyTableStep myStep = (VerifyTableStep)getStep();

        getContext().saveResponseAsCurrent(getDummyPage(HTML_PAGE_WITH_TABLE));

        myStep.setHtmlid("tbl");
        String expected = "  |Denomination |Prix unitaire |Qté |Total |\n"
                          + "|carotte      |1.00          |5   |5.00  |";
        myStep.addText(expected);

        executeStep(myStep);
    }

    

    @Override
    protected Step createStep() {
        return new VerifyTableStep();
    }
}

Volontairement, nous introduisons une assertion fausse dans l'implémentation , sinon l'exécution du test va réussir alors que l'implémentation n'est pas encore effectuée.

VerifyTableStep
Sélectionnez

public class VerifyTableStep extends Step {
    private String content = "";

    private String htmlid;


    @Override
    public void doExecute() throws Exception {
        Assert.assertTrue("non implémenté donc le test doit échouer", false) ;
    }
}

V-C-2-c. Exécution du test

Test unitaire de la classe VerifyTableStep
Test unitaire de la classe VerifyTableStep

V-C-2-d. Implémentation

L'implémentation consiste à parser d'un côté l'étalon et de l'autre côté le contenu du tableau HTML afin de les comparer.
Par ailleurs, nous rajouterons la possibilité d'exclure des colonnes (pratique dans notre tableau de notes où la colonne des opérations possibles sur les lignes n'est pas nécessaire à la comparaison). Enfin, nous génèrerons des messages d'erreurs explicites en cas d'échec.

Il en résulte les fichier sources suivants : VerifyTableStepTest.java, VerifyTableStep.java.

V-C-2-e. Intégration dans le projet mynotes

Une fois webtest-contrib redéployé sur le dépôt local maven, nous allons déclarer la balise VerifyTableStep dans le fichier Config.xml.

Config.xml
Sélectionnez

...
<taskdef name="verifyTable" classname="com.canoo.webtest.extension.VerifyTableStep" classpath="${runtime_classpath}"/>
...

Enfin relançons les tests en mode intégration et vérifions que le build ait réussi.

V-C-3. Téléchargement

Les sources de ces extensions sont disponibles via ftpftp ou bien via httphttp. Les librairies externes ont été regroupées dans une archive téléchargeable via ftpftp ou bien via httphttp.

V-D. Conclusion

Ce chapitre fût une nouvelle fois riche en développement. Nous avons d'avantage exploité Mockito. Dans une démarche "agile", nous avons pris l'initiative d'effectuer des développements externes au projet afin de nous faciliter l'exécution des tâches. Grâce aux tests unitaires, nous avons confiance en notre travail et la motivation ne fléchit pas. Le chapitre suivant montrera que nous encore gagnerons en vélocité grâce aux efforts déployés jusqu'ici.

V-E. Téléchargement

Les sources du projet de ce chapitre sont disponibles 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.