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.
Ci-dessous le script de création de table :
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. Écriture 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 trois 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.
<?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 :
<?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 |
|
Etapes du test webtest |
1 - Se connecter à l'application, |
|
Données en sortie |
[Note 0] Supermarché : Whisky, Vodka |
Concernant le fichier webtest, nous remarquons que l'assertion du contenu d'une cellule n'est pas très lisible.
<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. Écriture de la couche Dao▲
V-B-1-a. Écriture 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 :
<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().
...
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() :
@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 :
...
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. Écriture du test de sélection d'une note▲
Nous utiliserons les mêmes données en entrée que le test de selectAllByUser() :
@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.
...
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.
....
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. Écriture 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.
@Test
public
void
insertNote
(
) throws
Exception {
noteDao.insert
(
buildNote
(
new
String[]{
null
, "johndoe"
, "Java"
, "refactorer classe Beer"
}
));
assertTableOutput
(
"NoteDaoTest_insertNote.output"
, "NOTE"
);
}
<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().
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 :
...
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)).
À 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. Écriture 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 :
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 :
- Initialisation de NoteService et de NoteDao,
- Simulation de l'appel de la méthode selectAllByUser(),
- Assertion du résultat escompté,
- Assertion du nombre d'appels de la méthode selectAllByUser() de NoteDao.
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.
@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"
);
}
À 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-t-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. Écriture du test d'insertion d'une note▲
L'écriture est très similaire à ce que nous avons écrit précédemment.
@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 quelle 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. Écriture de la couche web▲
Nous allons définir la hiérarchie et la navigation des pages web :
V-B-3-a. Page principale des notes▲
V-B-3-a-i. Écriture du test▲
Nous allons asserter que la liste de notes et le lien Ajouter sont affichés.
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▲
L'erreur indique que la fixture tester n'a pas pu instancier la classe NotePage. Inspectons le code du constructeur LoggedInPage.
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 trois 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.
public
abstract
class
LoggedInPage extends
MyNotesPage {
protected
LoggedInPage
(
) {
if
(!
getMyNotesSession
(
).isAuthenticated
(
)) {
throw
new
RestartResponseAtInterceptPageException
(
HomePage.class
);
}
add
(
new
LogoutLink
(
));
}
private
class
LogoutLink extends
Link {
...}
}
<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.
public
class
WelcomePage extends
LoggedInPage {
public
WelcomePage
(
) {
User user =
getMyNotesSession
(
).getUser
(
);
add
(
new
Label
(
"user"
, user.getFullName
(
)));
}
<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é.
@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.
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().
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.
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.
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 :
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 :
@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
(
);
}
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▲
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);
}
}
}
}
<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▲
À 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.
@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.
public
class
NoteEditPage extends
LoggedInPage{
}
V-B-3-b-i. Exécution du test▲
V-B-3-b-ii. Implémentation▲
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);
}
}
}
<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é".
public
NoteEditPage
(
Note note) {
// TODO
}
V-B-3-b-iii. Réexécution du test▲
V-B-3-c. Suppression d'une note▲
V-B-3-c-i. Écriture du test▲
@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▲
...
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);
}
}
}
}
<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>
À 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
private
static
class
NoteLink extends
Link {
private
NoteLink
(
) {
super
(
"noteLink"
);
}
@Override
public
void
onClick
(
) {
setResponsePage
(
NotePage.class
);
}
}
<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é
connexion</a></li>
</ul>
</div>
<!-- Contenu -->
<wicket:
child/>
</div>
</wicket
:
extend>
@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 :
À 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 :
<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.
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. À 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. Écriture 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.
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 :
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▲
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.
...
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 fichiers source 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-la dans le profile integration de mynotes.
<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.
<taskdef
name
=
"webtestsuite"
classname
=
"com.canoo.ant.WebTestSuiteTask"
classpath
=
"${runtime_classpath}"
/>
Enfin, mettons à jour le fichier AllTests.xml.
<?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.
...
<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.
En ouvrant le rapport généré, nous constatons que le test NotesManagement.xml a été exécuté.
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 :
<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 :
<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) :
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. Écriture 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.
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.
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▲
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 fichiers source 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.
...
<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▲
V-D. Conclusion▲
Ce chapitre fut une nouvelle fois riche en développement. Nous avons davantage 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.