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/
]]>En effet, un bloc then permet de déclarer des conditions, des exceptions, des interactions et des définitions de variables là où un bloc expect ne peut contenir que des conditions et des déclarations de variables. L’écriture given/when/then est également plus intuitive dans le cas où vous souhaitez tester des stories. C’est également une des clés du Behavior Driven Development et une méthode saine pour structurer ses tests, qui oblige à réfléchir vraiment à ce que l’on teste. Ce que j’aime chez spock, c’est que c’est obligatoirement intégré via ces blocs, on ne peut pas faire autrement
Spock permet le data-driven testing mais c’est également un framework facilitant la création de bouchons/simulacres [Plus d'infos sur les différences bouchon/simulacre et test d'état/de comportement]. On s’intéresse ici au le test par comportement, c’est à dire qu’on va s’occuper des chainages des appels des méthodes entre elles et moins du résultat. On cherche alors à vérifier que l’appel à spockResource.findCalendarByDay(’1′) entraîne bien un unique appel à calendarDao.getInfosByDay(’1′).
def "test par comportement"() { given: def calendarDao = Mock(CalendarDao) def spockResource = new SpockResource(calendarDao) when : spockResource.findCalendarByDay("1") then : 1 * calendarDao.getInfosByDay("1") }
Le bloc given permet de définir les variables nécessaires à l’exécution du test. Ici, on bouchonne le dao que l’on affecte ensuite au service que l’on souhaite tester. Le bloc when correspond à l’appel de la méthode à tester.
Le bloc then comporte ici uniquement la condition à tester. La syntaxe veut dire on vérifie que la méthode calendarDao
.getInfosByDay est appelée uniquement une fois (1 *) avec le paramètre ’1′. Les paramètres sont évalués via l’appel à la méthode equals.
A la différence d’EasyMock, qui fonctionne par défaut avec des simulacres, Spock comme Mockito renvoie de base pour toutes les méthodes mockées sans spécification null ou 0 ou false. Ici par exemple, l’appel à
calendarDao .getInfosByDay("1")
renverra null. Pour spécifier une valeur de retour différente, il suffit d’utiliser la syntaxe suivante :
calendarDao .getInfosByDay(_) >> new SpockInfo("1");
Le même code avec EasyMock avec une valeur de retour :
@Test public void testEasyMock() { //given CalendarDao calendarDao = createNiceMock(CalendarDao.class); SpockResourcespockResource = new SpockResource( calendarDao); expect(calendarDao.getInfosByDay("1")).andReturn( new SpockInfo("1")); replay(calendarDao); //when SpockInfo spockInfo= spockResource.findCalendarByDay("1"); //then verify(calendarDao); }
Avec EasyMock, on annote la méthode par @Test [annotation JUnit] puis on crée le mock à l’aide de EasyMock.createNiceMock, pour avoir un mock lénient. On précise ensuite que l’on s’attend à ce que la méthode calendarDao.getInfosByDay(’1′) retourne l’objet new SpockInfo(’1′) avec expect(calendarDao.getInfosByDay(’1′)).andReturn(new SpockInfo(’1′)); . On ‘charge’ ensuite les mocks via le replay et à la ligne suivante on lance l’appel à la méthode testée. Le verify à la dernière ligne permet de vérifier qu’un unique appel à la méthode a bien été effectué.
L’inconvénient de l’utilisation des simulacres, c’est que les tests et le code testé sont très (trop!) liés. Ainsi une modification du code peut entraîner beaucoup de refactoring au niveau des tests sans valeur ajouté. L’exemple le plus marquant est la séparation d’une méthode en deux méthodes distinctes : il faut reprendre tous les simulacres alors que ce n’est qu’une modification de ‘clarté’. Il est donc souvent préférable de ne pas tester le comportement mais uniquement le résultat à chaque fois que cela est possible et judicieux, c’est à dire du faire du test sur l’état des objets.
def "test stub avec retour"() { given: def calendarDao = Mock(CalendarDao) def spockResource = new SpockResource(calendarDao) when : def spockInfo = spockResource.findCalendarByDay("1") then : calendarDao .getInfosByDay("1") >> new SpockInfo("1"); spockInfo.day == 1 }
Ici, on ne fait plus de contrôle sur le nombre d’appel à la méthode getInfosByDay(’1′). On indique juste que lorsque cette méthode est appelée, on renvoie (>>) une nouvelle instance de SpockInfo. Ici pas de assertEquals ou autre méthode du genre, le spockInfo.day==1 est en fait un raccourci pour écrire assert spockInfo.day == 1.On vérifie que la variable day de l’objet spockInfo est bien égale à 1.
Voilà le code équivalent avec Mockito :
@Test public void testMockito() { //Given CalendarDao calendarDao = mock(CalendarDao.class); SpockResource spockResource = new SpockResource( calendarDao); when(calendarDao.getInfosByDay("1")).thenReturn( new SpockData("1")); //when SpockData spockData= spockResource.findCalendarByDay("1"); //then assertEquals("1", spockData.getDay()); }
A la première ligne, on construit le bouchon calendarDao que l’on affecte à la ligne suivante à l’objet spockResource. On indique au bouchon calendarDao que quand la méthode getInfosByDay est appelée avec le paramètre ’1′, alors elle retourne new SpockData(’1′). On effectue ensuite l’appel à la méthode testée spockResource.findCalendarByDay(’1′) et on vérifie que la variable day du résultat spockData est bien égale à 1.
Et si on veut chainer les retours ?
Il est parfois nécessaire de renvoyer des valeurs différentes pour une même méthode bouchonnée. Pour les 3 cas suivants, le premier appel à la méthode calendarDao.getInfosByDay avec le paramètre ’1′ renverra SpockInfo(’1′), le deuxième new SpockInfo(’2′) :
Avec Easymock :
expect(calendarDao.getInfosByDay("1")).andReturn(new SpockInfo("1")).andReturn(new SpockInfo("2"))
Avec Mockito :
when(calendarDao.getInfosByDay("1")).thenReturn(new SpockData("1")).thenReturn(new SpockInfo("2"))
Avec Spock :
calendarDao.getInfosByDay("1") >>> [new SpockInfo("1"),new SpockInfo("2")];
Le prochain article abordera les fonctionnalités plus avancées de la gestion des arguments des fonctions mockées (ArgumentMatcher) également dans les trois frameworks.
Crédit Photo : Mistinguette18
]]>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.
]]>