Il est possible d’utiliser des wildcards au niveau des arguments appelés. Ainsi 1 * calendarDao
.getInfosByDay(_) veut dire ‘la méthode calendarDao.getInfosByDay est appelée une fois (1 *) avec n’importe quel paramètre (_). On peut également spécifier la classe de l’argument : 1 * calendarDao.getInfosByDay(_ as String) .
Il est également possible par exemple de compter le nombre d’appel aux méthodes d’un simulacre (quel quel soit : 3 * calendarDao._ passera uniquement si les instructions du bloc when font exactement appel à 3 méthodes de l’instance calendarDao. Il est également possible de donner des intervalles plutôt qu’une valeur, ce qui n’est pas permis par les autres frameworks (à part le ‘au moins 1 fois’) :
(1..3) * calendarDao.getInfosByDay(_) [entre 1 et 3 fois]
(5.._) * calendarDao.getInfosByDay(_) [=> au moins 5 fois]
(_..5) * calendarDao.getInfosByDay(_) [=> au plus 5 fois]
Il existe de nombreux wildcard, la plupart ne servent pas à grand chose. Ceux qui me semblent le plus important :
calendarDao.getInfosByDay(_) : n’importe quel argument
calendarDao.getInfosByDay(!null) : n’importe quel argument non null
calendarDao.getInfosByDay(_ as String) tous les éléments de type String
Equivalent EasyMock
expect(calendarDao.getInfosByDay((String)anyObject())) : n’importe quel argument, obligation d’être une String
expect(calendarDao.getInfosByDay((String)notNull())): n’importe quel argument non null
expect(calendarDao.getInfosByDay(isA(String.class))):tous les élements de type String
Equivalent Mockito
when(calendarDao.getInfosByDay(anyString())) : n’importe quel argument, obligation d’être une String
when(calendarDao.getInfosByDay((String)notNull())): n’importe quel argument non null
when(calendarDao.getInfosByDay(isA(String.class))): tous les élements de type String
Vous pouvez trouver la liste ici : http://code.google.com/p/spock/wiki/Interactions
Les contraintes personnalisées sont très utiles dans certains cas, par exemple lorsque la méthode equals est déjà défini dans le code et qu’elle ne correspond pas à notre besoin ou que nous avons par exemple un champ date que nous souhaitons exclure de la comparaison. En règle général, il vaut mieux redéfinir la méthode equals qui est spontannément utilisée par les 3 frameworks pour comparer l’égalité des arguments attendus et reçus.
Nous allons chercher avec les 3 frameworks à créer des contraintes personnalisés (spock) ou des argument matcher (Mockito & Easymock). Notre but est de faire en sorte que l’appel à listSpockData.add avec un paramètre ayant comme variable de classe img égale à « a » soit bien simulée. Pour cela, nous créons un objet spockData ayant bien img = « a » ainsi qu’une liste d’objets SpockData. Nous appelons ensuite la méthode listSpockData avec l’objet spockData et vérifions que cette dernière a bien été appelée.
Avec Spock :
def "should list Spock Data"() { given: SpockData spockData = new SpockData("a", "accroche", "details", 6); List listSpockData = Mock(); when : listSpockData.add spockData; then : 1*listSpockData.add({it.img=="a"}) }
Spock permet via les conditions particulières de définir un bloc à l’aide d’une closure , avec { } et de définir à l’intérieur plusieurs conditions. It signifie ici l’objet qui sera passé en paramètre à la méthode. Il est possible d’utiliser plusieurs expression (it.img== »img »&&it.day==6) ou une fonction définie dans la classe de test.
Avec Mockito, qui utilise en fait les ArgumentMatcher du framework Hamcrest.
class isImgEqualToA extends ArgumentMatcher { //creation d un argument matcher public boolean matches(Object spockData) { return ((SpockData) spockData).getImg() == "a"; } } @Test public void testArgumentMockito(){ List mock = mock(List.class); when(mock.add(argThat(new isImgEqualToA()))).thenReturn(true); mock.add(newSpockData("a", "b", "c", 2)); verify(mock).add(argThat(new isImgEqualToA())); }
Avec Easymock, l’opération se révèle être très verbeuse. [Pour voir une implémentation plus conforme] :
static class Matcher implements IArgumentMatcher { //creation du matcher @Override// implementer cette methode permet de definir // un message d erreur public void appendTo(StringBuffer arg0) { } @Override// definition de la methode qui verifiera que // l argument img est bien egal a A. smell code. public boolean matches(Object spockData) { return ((SpockData) spockData).getImg() == "a"; } // definition d une methode static pour déclarer le matcher public static SpockData isImgEqualToA() { EasyMock.reportMatcher(new Matcher()); return null; } } @Test //Test public void testArgumentEasyMock() { List mock = createMock(List.class); expect(mock.add(Matcher.isImgEqualToA())).andReturn(true); replay(mock); mock.add(new SpockData("a", "b", "c", 2)); verify(mock); }
Les tests mockito et easymock sont bien plus verbeux et au final sont plus restrictifs car elles utilisent des classes séparées pour définir les matchers. Spock évite la lourdeur d’avoir à définir une autre classe
Néanmoins, dans le cas de tests avec ArgumentMatcher, EasyMock & Mockito permettent de définir des messages d’erreurs personnalisés, via la méthode appendTo pour le premier et describeTo pour le deuxième. Notons que pour le premier, il faut la coder nous même alors que le deuxième en propose une par défaut construite à partir du nom de la classe isImgEqualToA donne ‘Img equal to A’. En règle général, on affiche un toString() de l’objet pour aider au debuggage et on regarde alors les 2 chaînes pour trouver les différences (ou en pas à pas en debug). Spock ne propose rien de tel dans sa version actuelle (0.4) mais en version 0.5 il est prévu de pouvoir utiliser les matchers d’Hamcrest, revenant à avoir la même syntaxe qu’avec Mockito, en un peu plus courte.
Crédit Photo : Oskay – http://www.flickr.com/photos/oskay/339996940/sizes/m/in/photostream/
]]>def "String param should correspond to numeric spockInfoDay - classical syntax"() { setup: def spockResource = new SpockResource(new CalendarDaoStatic()) expect: spockResource.findCalendarByDay(day).day == dayNumeric where: day << ["1", "2", "3"] dayNumeric << [1, 2, 3] }
Le test est organisé en 3 blocs : setup, expect et where le tout placé dans un objet dont le nom est défini par def « nom du test ». Le premier bloc setup sert à déclarer les variables qui vont être utilisées dans la suite du test. Ici c’est par exemple l’instanciation de l’objet spockResource. Dans le bloc where, je définis deux variables : l’une day qui prendra successivement les valeurs ‘1’,’2′ et ‘3’ et l’autre dayNumeric les valeurs 1,2,3 [groovy est un langage dynamique donc pas besoin d’indiquer le type des variables, il sera déterminé automatiquement]. Dans le bloc expect, j’indique mon test : je vérifie que la méthode spcokResource.findCalendarByDay retourne bien un objet comportant un attribut day dont la valeur correspond à la valeur en tant qu’entier d’une chaîne de caractère [c’est à dire que ma fonctionnalité ne fait pas grand chose d’autre qu’un Integer.valueOf]. Lors de l’exécution, il y a en réalité 3 tests JUnit qui sont exécutés, un pour chaque couple de paramètre day=’1′ & dayNumeric=1 / day=’2′ & dayNumeric=2 / day=’3′ & dayNumeric=3
Le même test peut être écrit d’une manière différente en utilisant une autre syntaxe, encore plus lisible.
def "String param should correspond to numeric spockInfoDay"() { setup: def spockResource = new SpockResource(new CalendarDaoStatic()) expect: spockResource.findCalendarByDay(day).day == dayNumeric where: day | dayNumeric "1" | 1 "2" | 2 "3" | 3 }
Ici même principe, 3 tests seront joués , avec les paramètres day = ‘1’ et dayNumeric = 1 / day = ‘2’ et dayNumeric = 2 /day = ‘3’ et dayNumeric = 3 . Les paramètres étant les uns à la suite des autres et séparés par des | cela permet de mieux visualiser ses données de test.
Le choix entre la première et la deuxième méthode dépend du contexte. Si les données à tester sont statiques, la deuxième méthode est plus claire. Mais la première méthode permet de tester avec des données dynamiques comme par exemple :
where: [a, b, c] << sql.rows("select a, b, c from maxdata")
Ces deux syntaxes permettent de faire du data-driven testing, c’est à dire du test piloté par les données : il est possible de vérifier plusieurs test cases en injectant les données de départ et les données attendues via une source externe, ici le bloc where. Nettement plus simple que de lancer trois tests JUnit différent pour le même comportement, le tout en restant très lisible.
Si on regarde l’équivalent JUnit
@RunWith(Parameterized.class) public class DataDrivenSimpleTest { private Integer day; private Integer dayNumeric; @Parameters public static Collection
Voilà l’exemple avec l’annotation @Parameters incluse dans JUnit depuis la version 4.0 et il n’y a que l’essentiel pour tester la méthode findCalendarByDay. Tout d’abord, la classe doit être lancée avec un runner spécifique Parameterized.class (à la ligne 1). Elle a besoin de deux variables de classes day et dayNumeric ainsi que d’un constructeur qui initialise ses deux variables. Il y a aussi besoin d’une méthode public static qui retourne une collection d’object représentant les différentes données pouvant être prises par les deux paramètres day et dayNumeric annotée avec @Parameters. Seulement ensuite apparait la méthode de test, shouldReturnTheNumericValueOfDay qui utilise les variables de classes day et dayNumeric. Il est également possible dans la méthode annotée par @Parameters de définir de manière dynamique des jeux de données, on peut par exemple penser à l’importation de données à partir d’un fichier excel ou d’une requête SQL par exemple comme avec Spock. Outre la verbosité de cette méthode, il n’est possible que d’avoir un seul test paramétré par classe.
Les pré-requis à l’utilisation des tests paramétrés avec JUnit (variables de classe, runner, constructeur …) font que je préfère largement utiliser Spock pour faire du data-driven testing.
]]>– http://www.waterfallmanifesto.org : Site parodique sur les anciennes méthodes de développement (mais hélas toujours d’actualités) .
– http://blog.crisp.se : Vue artistique d’une journée de dev avec la méthode Kanban.
– http://www.scala-lang.org/ : Nouveau langage à typage fort, à la fois objet et fonctionnel, qui fonctionne sur la plateforme Java (compilé en bytecode). Formidable language sur le papier, je reste perplexe quant à son adoption par le plus grand nombre.
– http://www.kiad.org/ : Blog généraliste du PDG d’Owlient. Articles sur le Web de très bonne qualité.
– Bases de données dénormalisées : Article très instructif sur la nouvelle génération de bases de données dites « Dénormalisées », utiles pour les applications effectuant un très grand nombre de lectures et peu de modification.
]]>