Posts Tagged ‘TDD’
EDIT: Hop. Enfin la release 1.9.0 est dispo en téléchargement.
Après pas mal de travail avec des périodes plus ou moins intenses – bref les vicissitudes du développement Open Source – le projet sort une nouvelle version 1.9.0 en Release Candidate, avec des bugfixes et bien sûr des nouvelles features. Il y a un changelog mais dans les faits le billet suivant liste brièvement ce qui est nouveau. Ah oui la version est téléchargeable ici et bientôt disponible sur le central maven.
- Pour être plus fluent et expressif, l’API introduit les alias then et will pour les réponses personnalisées (Answer). Ainsi que d’autres petits tweak de l’API:
@Test public void engine_should_only_work_with_diesel() { given(engine.start()).will(throwExceptionIfEssenceInsteadOfDiesel()); // ... } private Answer throwExceptionIfEssenceInsteadOfDiesel() { return new Answer<EngineStatus>() { public EngineStatus answer(InvocationOnMock invocation) { // answer code } }; } - Les mocks peuvent maintenant être déclaré dans la configuration du stub, sur une ligne.
DieselEngine de = given(mock(DieselEngine.class).start()).willThrow(TankIsEmpty.class).getMock();
- On peut maintenant renvoyer la classe d’une exception plutôt que son instance.
given(someMock).willThrow(IllegalArgumentException.class, SomethingIsWrongException.class);
- Si jamais vous avez besoin de debugguer un bout de code ou les interactions sont non prédictibles, il est maintenant possible de loguer les invocations du mock ou de l’espion. Attention, bien qu’utile à l’occasion avec du code legacy, quand même si jamais ce besoin s’en fait sentir sur un nouveau développement c’est que ce code devient trop complexe.
List mockedList = mock(List.class, withSettings().verboseLogging()); mockedList.get(0);
On pourra également ajouter des callbacks sur chaque interaction du mock.
Observer observer = mock(Observer.class, withSettings().invocationListeners(listener1, listener2)); willThrow(IllegalArgumentException.class).given(observer.update(observable, "what has changed"));
- Pas mal de travail a été fait sur les annotations. Maintenant il n’est plus nécéssaire d’initialiser un champ annoté par @Spy s’il existe dans la classe un constructeur sans argument.
@RunWith(MockitoJUnitRunner.class) public class SomeTest { // pas besoin d'initialiser le champs @Spy private ArrayList spiedArrayList; @Test public void verify_some_interactions() { spiedArrayList.iterator(); verify(spiedArrayList, once()).iterator(); } } - Et pour la fin mais pas des moindres, le mécanisme d’injection de mockito supporte maintenant l’injection par constructeur. A l’heure actuelle, seul les mocks et spies déclaré dans le test en tant que champs pourront être injecté dans le constructeur du champs annoté par @InjectMocks.
@RunWith(MockitoJUnitRunner.class) public class EngineTest { @Mock Diesel diesel; @InjectMocks Engine engine; @Test public void engine_should_consume_Diesel() { engine.start(); } }Ou Engine a un constructeur avec le paramètre Diesel.
public class Engine { Diesel diesel; public Engine(Diesel diesel) { this.diesel = diesel; } public boolean start() { checkNotEmpty(diesel); // ... } // ... }
Pour l’instant en RC, cette release permettra d’adoucir les angles si nous en avons loupé certains éléments. N’hésitez pas à nous poser des questions sur la mailing list ou stackoverflow.
Hello, j’en avais un peu marre d’écrire régulièrement voire répétitivement dans mes tests les constructions mockito.
Pour ça je me suis créé dans mon IDE favori, IntelliJ, ce qu’on appelle des Live Template. Ces templates permettent à partir d’une abréviation d’insérer des fragments de code. Ainsi par exemple :
Taper iter dans votre éditeur puis de faire Ctrl+J (sous OSX) va développer cette abréviation dans le bout de code ci-dessous (suivant le contexte bien entendu)
for (TypeInIterable type : someIterable) {
}
Taper sur Ctrl+J (sous OSX) vous permet de lister les abréviations disponible dans le contexte courant.
Les Live Template pour Mockito
Bien qu’imparafaite pour des raisons de limite technique d’IntelliJ, elles sauvent un minimum de temps, multiplié par le nombre de test. Malheureusement il n’y a pas non plus d’import export uniquement pour les live template, il faut donc se taper la configuration de intellij à la main. Cela dit il est possible de contourner partiellement ce problème avec la sauvegarde de la configuration personnelle sur les serveurs intellij, ou encore d’exporter la configuration pour les live templates, les file templates, et encore autre chose.
J’ai défini toutes ces annotations dans un nouveau groupe ‘test’, et j’ai activé pour toutes le contexte Java, avec reformatage et simplification du nom qualifié.
- Description : Creates a field with the @Mock annotation
Abbréviation : ‘am’
Template text :@org.mockito.Mock private $TYPE$ $MOCK_FIELD$
Les variables du templates sont :
Name Expression Default value Skip if defined TYPE variableOfType(“Object”) MOCK_FIELD suggestVariableName() - Description : Creates a field with the @Spy annotation
Abbréviation : ‘as’
Template text :@org.mockito.Spy private $TYPE$ $MOCK_FIELD$
Les variables du templates sont :
Name Expression Default value Skip if defined TYPE variableOfType(“Object”) MOCK_FIELD suggestVariableName() - Description : Creates a field with the @InjectMocks annotation
Abbréviation : ‘aim’
Template text :@org.mockito.InjectMocks private $TYPE$ $MOCK_FIELD$
Les variables du templates sont :
Name Expression Default value Skip if defined TYPE variableOfType(“Object”) MOCK_FIELD suggestVariableName() - Description : Add @RunWith(MockitoJUnitRunner.class)
Abbréviation : ‘rwm’
Template text :@org.junit.runner.RunWith(org.mockito.runners.MockitoJUnitRunner.class)
- Description : BDD Stub mock with given(…).willReturn(…) style
Abbréviation : ‘gw’
Template text :given($MOCK$).willReturn($ARGS$)$END$
Les variables du templates sont :
Name Expression Default value Skip if defined MOCK variableOfType(“Object”) ARGS - Description : BDD Stub spy/mock with willReturn(…).given(…) style
Abbréviation : ‘wg’
Template text :org.mockito.BDDMockito.willReturn($RETURNED$).given($MOCK$).$CALL$ $END$
Les variables du templates sont :
Name Expression Default value Skip if defined RETURNED complete() MOCK variableOfType(“Object”) CALL complete() - Description : Inserts a verify(…) statement
Abbréviation :‘verif’
Template text :org.mockito.Mockito.verify($MOCK$).$CALL$
Les variables du templates sont :
Name Expression Default value Skip if defined MOCK variableOfType(“Object”) CALL complete() - Description : Inserts Mockito.inOrder(mocks) followed by inOrder.verify(…) statement
Abbréviation : ‘ioverif’
Template text :org.mockito.InOrder $inOrderVar$ = org.mockito.Mockito.inOrder($MOCKS$); $IN_ORDER_VAR$.verify($MOCK$).$CALL$;
Les variables du templates sont :
Name Expression Default value Skip if defined IN_ORDER_VAR suggestVariableName() MOCKS variableOfType(“Object”) MOCK variableOfType(“Object”) CALL complete() - Description :Inserts a verify(…) statement
Abbréviation :‘verif’
Template text :$IN_ORDER_VAR$.verify($MOCK$).$CALL$;
Les variables du templates sont :
Name Expression Default value Skip if defined IN_ORDER_VAR variableOfType(“org.mockito.InOrder”) MOCK variableOfType(“Object”) CALL complete()
Voilà donc les templates que je me suis créé pour IntelliJ, il manque certainement des cas d’utilisation, mais je trouvais plus judicieux de mettre ces cas là au moins. Pour nos amis Eclipse oou Netbeans, il y a des fonctionnalités comparables plus ou moins évoluées (de mémoire le système d’Eclipse est plutôt pas mal).
Références
Vous devez écrire du code qui fait appel à JMX, en bon citoyen et bon développeur vous voulez tester ce code.
Première approche; vous enregistrez vos MBean sur un MBeanServer, disons celui de la plateforme (avec Java 6 : ManagementFactory.getPlatformMBeanServer()).
mBeanServer.registerMBean(theMBean, theMBean.getObjectName());
Étant donné que MBeanServer étends MBeanServerConnection il est possible d’exécuter des querys, de faire des invocations sur les MBean etc. Si le code est suffisamment isolé des aspects techniques de connexion à JMX, vous passerez le MBeanServer en lieu et place de la MBeanServerConnection.
Supposons le code suivant.
public class OperateOnJMXConnection implements JMXOperation {
public void perform(MBeanServerConnection connection) {
// doing some stuff there
}
public Result getResult() { return result; }
}
Pour tester ce code il faudrait alors écrire :
@Test
public void do_not_fail() {
operateOnJMXConnection.perform(mbeanServer);
assertThat(result).satisfies(someCondition);
}
Mais voilà, vous restez en local, et par exemple si vous avez merdé sur la sérialisation de vos beans, vous ne verrez pas d’échec dans vos test et vous aurez une surprise en prod, ou avant si votre projet a un processus qualité décent.
Évidement il y a une solution, l’idée c’est de pouvoir se connecter au mBeanServer local à votre processus (typiquement dans maven 3, l’exécution de vos tests peuvent être forkée).
Alors j’ai essayé de récupérer les informations pour récupérer les informations de la VM qui tourne, mais bon on tombe dans des classes sun, j’ai préféré ne pas continuer sur ce chemin semé d’embûches, sans compter sur la faiblesse de cette approche.
Bref en relisant les articles de Khanh sur JMX, j’ai vu quelque chose d’intéressant JMXConnectorServerFactory. Cette classe permet donc de créer un JMXConnectorServer avec l’URL qu’on lui spécifie et d’un MBeanServer. A noter que cette URL doit respecter un certain formalisme tel que la javadoc l’indique : service:jmx:protocol:remainder.
Le protocole ne peut pas être n’importe quoi, il faut qu’il y ait le bon service enregistré pour qu’il soit géré. Dans notre cas RMI est standard, c’est donc le protocole que je prendrai. Pour le remainder, il s’agit plus d’une partie d’une URL, je vous laisse voir la Javadoc de JMXServiceUrl à ce sujet, mais dans les grandes lignes la forme doit être la suivante : //[host[:port]][url-path]
JMXConnectorServer connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(
new JMXServiceURL("service:jmx:rmi://"),
null,
mBeanServer
);
connectorServer.start();
Hop dans le code précédent, on a créé puis démarrer notre JMXConnectorServer. Il n’y a plus qu’à se connecter dessus de manière standard :
Je vais utiliser connectorServer.getJMXServer() pour récupérer l’URL du service, il y a une raison à cela, c’est que comme l’indique la javadoc, l’URL passée pour la création du JMXConnectorServer peut être légèrement modifiée par celui-ci, il faut donc récupérer la nouvelle URL.
JMXConnector jmxConnetor = JMXConnectorFactory.connect(connectorServer.getJMXServiceUrl()); MBeanServerConnection connection = jmx.getgetMBeanServerConnection();
Et voilà vous avez accès à une MBeanServerConnection, qui vit dans la JVM locale, mais qui utilise RMI pour communiquer avec le MBeanServer, du coup vous êtes nettement plus proches des conditions du code de production et c’est ce qui nous intéresse dans cet article.
Pour référence les articles de Khanh, et en français s’il vous plait
:
- Partie 1 : http://jetoile.blogspot.com/2010/10/jmx-pour-les-nuls-les-concepts-partie-1.html
- Partie 2 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-differents-mbeans.html
- Partie 3 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-agents-jmx-partie.html
- Partie 4 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-classes-de-base.html
- Partie 5 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-le-mbean-server.html
- Partie 6 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-chargement-dynamique.html
- Partie 7 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-les-services-jmx.html
- Partie 8 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-les-connecteurs.html
Quelques liens javadoc :
Enfin je me suis créé une petite classe de commodité qui permet de créé facilement un loopback pour les TU :
Le voilà il est sorti. Ce framework de mock digne épigone de easymock, a su plaire et propose plus que son ancêtre. Mais quoi de neuf qui vaut la peine d’être mentionné?!
Extensions des annotations
Le code ci-dessous présente le support étendu des annotations.
- @InjectMock, qui permet donc d’injecter les bouchons et les espions dans l’instance de ce champs. Il s’agit le plus souvent de classe testée. Attention la classe instanciée ne doit pas être nulle.
- @Mock a été étendu pour fournir des paramètres, équivalent aux possibilités offertes par withSettings, par exemple :
Mockito.mock(Class<?>, Mockito.withSettings().name("a mock for bob")) - @Spy qui comme son nom l’indique permet de créer un espion à partir d’une instance déjà créé.
- @Captor qui permet d’instancier un ArgumentCaptor.
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertSame;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class BusinessTestStuff {
@InjectMocks
private BusinessStuff stuffToTest = new BusinessStuff();
@Mock(name = "businessMethods")
private List<String> methodList;
@Mock(name = "technicalMethods")
private List<String> methodList2;
@Spy
private Map<Class<?>, Object> classInstances = new HashMap<Class<?>, Object>();
@Captor
private ArgumentCaptor<String> methodCaptor;
@Test
public void shouldDoSomethingWithBusinessAndTechnicalMethod() {
// given
given(methodList.get(15)).willReturn("businessMethod");
given(methodList2.get(15)).willReturn("technicalMethod");
// when
stuffToTest.doSomething(15);
// then
// ne lève pas d'exception
}
@Test
public void shouldCaptArgument() {
// given
// when
stuffToTest.addBusinessStuff("businessMethod");
// then
verify(methodList).add(methodCaptor.capture());
assertSame("businessMethod", methodCaptor.getValue());
}
@Test
public void shouldSpyMyMap() {
// given
// when
classInstances.put(String.class, "instance");
// then
verify(classInstances).put(eq(String.class), any(String.class));
}
}
Pourquoi ces annotations? Eh bien afin de rendre les tests plus clairs. Faire du beau code dans le code de production ne suffit pas, il faut faire attention à la clareté et l’expressivité de ces tests. Il ne faut pas oublier qu’on écrit seulement une fois du code et qu’on le relit bien plus. Parlez à vos équipe de maintenance ou d ‘évolutions combien de temps il passent à comprendre ce que vous avez voulu coder.
L’idée c’est d’écrire du code le plus propre possible, mais d’avoir aussi les tests les plus propres possibles, ceci aidera à comprendre à votre lecteur quelle(s) responsabilité(s) et quelle(s) comportement(s) son attendu.
Pour tester du code legacy
Ah voilà qui va en intéresser plus d’un. Pour votre ancien code ou vous devez traverser une grappe d’objet pour mocker le dernier, c’était plutôt fastidieux.
Mockito.when(mockedLegacyCode.getTop()).thenReturn(mockedTop);
Mockito.when(mockedTop.getMiddle()).thenReturn(mockedBottomRight);
Mockito.when(mockedBottomRight.getId()).thenReturn("BR");
Pour information, depuis longtemps déjà Mockito supporte la création de mock avec des réponses prédéfinies. Jusque là par défaut, les mock retournait les valeur par défaut, 0 pour un entier, null pour une référence, etc… Typiquement on évitais les null avec la réponse, qui renvoyait donc des mocks :
Mockito.mock(LegacyCode.class, Mockito.RETURNS_MOCKS);
Cependant la limite de cette réponse était qu’on ne pouvait pas créer des comportements pour des objets profonds dans la grappe d’objet. Et c’est là que la version 1.8.3 fournit un petit ajout assez sympa, on pourra maintenant écrire des choses comme :
CrapyLegacy mock = Mockito.mock(CrapyLegacy.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(mock.getTop().getMiddle().getBottomRight().getId()).thenReturn("deep id");
assertEquals("deep id", mock.getTop().getMiddle().getBottomRight().getId());
De la même manière avec les annotations étendues, on déclarera le mock de cette façon :
@Mock(answer = RETURNS_DEEP_STUBS) private CrapyLegacy mock;
Attention cependant, ceci est une facilité pour tester le code legacy, utiliser cette facilité aujourd’hui pour du nouveau est un signe grave que vous êtes en train de développer du code Legacy!
En effet vous jouez avec la loi de Demeter, et quand on joue à la loi de Demeter c’est très souvent parce que le code créé n’est pas orienté objet : il s’agit de code procédural! Le code procédural peut coûter cher voire très cher à maintenir et à faire évoluer [1]. C’est un des élément de la dette technique et financière qui coutera plus cher d’année en année!
Un mot sur le TDD
Mockito s’enrichit, fournit des facilités, permet d’éclaircir le code, pour véritablement être dans l’esprit TDD. L’esprit TDD ce n’est pas de faire de la couverture de code, ce n’est pas d’écrire du code puis de le tester après, ce n’est pas de tester la mécanique interne d’une classe. C’est juste commencer par écrire un test simple pour exprimer une responsabilité et un comportement qu’on attend sur un objet, puis d’enrichir le code et le test associé.
Si vous voulez faire du reverse engineering ou essayer des framework inconnus, commencez par écrire un test qui correspond à ce que vous attendez, puis refactorer petit à petit le code testé et le test. Le code s’enrichira petit à petit, si ça devient difficile peut-être que voues êtes face à un problème de découpage de responsabilités, ça peut induire de créer de nouveaux objets et donc de spliter le test également. Ce faisant vous réduisez la complexité du code et du test.
Si vous le nom de vos classes ou de vos méthodes ne sont pas satisfaisant c’est que les responsabilités ne sont pas clairement définies/identifiées, un refactoring est à prévoir.
Au début c’est un peu difficile, mais avec un peu d’exercice on devient meilleur! N’est-ce pas le but de l’agilité, de s’améliorer!
MAJ : Source [1] : http://www3.interscience.wiley.com/journal/114082374/abstract?CRETRY=1&SRETRY=0



Follow