Mockito 1.8.3

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 devrait 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 :

[java]Mockito.mock(LegacyCode.class, Mockito.RETURNS_MOCKS);[/java]

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!

Source [1] : http://www3.interscience.wiley.com/journal/114082374/abstract?CRETRY=1&SRETRY=0