Chiron-mockster

9 views
Skip to first unread message

Laurent Caillette

unread,
Jun 30, 2018, 11:50:04 PM6/30/18
to tec...@googlegroups.com

JMockit a longtemps été ma bibliothèque favorite pour des similis ("mocks") parce qu'il y a moyen de vérifier que toutes les invocations souhaitées ont eu lieu, pas une de plus et pas une de moins. Et il est capable de redéfinir des comportements réputés non-redéfinissables, genre ``java.lang.System.currentTimeMillis()``.

Mais sur "Chiron"
, la migration de `JMockit-1.20` vers `1.39` a été un véritable carnage, avec des comportements bizarres pour tous les tests avec des accès concurrents sur les similis. 

Je rappelle brièvement le fonctionnement de JMockit. JMockit oriente l'écriture des tests vers une séquence enregistre-rejoue-vérifie. Pour indiquer quelle valeur devra renvoyer un simili ça se fait lors de l'enregistrement. 

Pour les exemples nous utiliserons un simili pour le contrat suivant (tout le code exemple est disponible "ici"
) :

<<<
  interface Engine {
    long addSync( final int term1, final int term2 ) ;

    void addAsync(
        final int term1,
        final int term2,
        final LongConsumer resultConsumer
    ) ;
  }
>>>

La phase enregistrement ressemble à ça :

<<<
  @Test
  public void record( @Injectable final Engine engine ) {
    new Expectations() {{
      engine.addSync( 3, 2 ) ;
      result = 5 ;
    }} ;
    
    // System under test + assertion.
    assertThat( engine.addSync( 3, 2 ) ).isEqualTo( 5 ) ;
  }
>>> 

Dans le bloc ``Expectations`` on dit quelle valeurs on attend pour les paramètres de ``addSync(_)``, mais on peut aussi utiliser ``any``. La valeur retournée est 144, indiquée par l'assignation de ``result``.

Notons quelques gracieusetés syntaxiques :
- La déclaration d'un simili comme paramètre de la méthode de test.
- L'initialiseur anonyme dans la classe dérivée de ``Expectations`` qui restreint l'accès à certains symboles magiques comme ``any`` ou ``result`` à la portée de l'initialiseur.

On peut aussi vérifier que certaines opérations ont bien eu lieu :

<<<
  @Test
  public void verify( @Injectable final Engine engine ) {
    engine.addSync( 3, 2 ) ;  // Result is 0.

    new Verifications() {{
      engine.addSync( 3, 2 ) ;
    }} ;
  }
>>>

Mais là pas moyen de s'intéresser à la valeur de retour, quand on appelle la méthode du simili JMockit fournit une valeur par défaut (0 ou ``null``). On peut alterner les ``Expectations`` et les ``Verifications``, avec une ``FullVerifications`` qui garantit que seul les appels attendus ont eu lieu.

Maintenant nous allons corser le truc avec une fonction de rappel.

<<<
  @Test
  public void capture( @Injectable final Engine engine ) throws InterruptedException {
    final LongConsumer resultConsumer = l -> { } ;

    // What System Under Test could do.
    final Thread computationRunner = new Thread(
        () -> {
          LOGGER.info( "Sleeping a bit ..." ) ;
          Uninterruptibles.sleepUninterruptibly( 1, TimeUnit.SECONDS ) ;
          LOGGER.info( "Now we perform addition ..." ) ;
          engine.addAsync( 12, 2, resultConsumer ) ;
          LOGGER.info( "Addition complete." ) ;
        },
        "computation-runner"
    ) ;
    computationRunner.start() ;

    new Verifications() {{
      final LongConsumer captured ;
      LOGGER.info( "Verifying ..." ) ;
      engine.addAsync( 12, 2, captured = withCapture() ) ;
      LOGGER.info( "Verification complete." ) ;
      assertThat( captured ).isSameAs( resultConsumer ) ;
    }} ;

    computationRunner.join() ;
  }
>>>

L'intention est simple : on récupère dans ``captured`` la valeur passée au simili à partir d'un autre fil d'exécution. C'est là que ça foire : JMockit ne sait pas attendre que la méthode ``addAsync`` soit appelée, donc il fournit une valeur par défaut :

<<<
java.lang.AssertionError: 
Expecting:
 <com.otcdlink.chiron.fixture.demo.JMockitDemo$$Lambda$1/1579526446@1c72da34>
and actual:
 <null>
to refer to the same object
>>>

Dans ce cas on voudrait que dans les ``Verifications`` l'appel à ``addAsync`` bloque jusqu'à ce que le véritable appel ait lieu. Mais à la décharge de JMockit il y a des cas où cela générerait des interblocages, notamment si on a des similis interdépendants.

(Oui j'ai aussi essayé le ``withCapture( List )`` avec une liste se comportant comme une ``BlockingQueue``) mais ça ne se passait pas très bien avec plusieurs captures, parce qu'on ne peut définir une capture qu'une seule fois, donc on ne voit plus dans la séquence du test à quel moment elles doivent avoir lieu.)

Je passe d'autres comportements plus obscurs, mais il est clair que JMockit n'est pas adapté à des tests impliquant de la concurrence d'accès sur les similis.

Donc la solution c'est d'écrire sa propre bibliothèque de similis (une de plus) et c'est ce qui me vaut l'honneur de présenter "Chiron-mockster"

Cette bibliothèque est destinée à un cas d'utilisation très restreint :
- Tout le code à tester s'exécute dans un autre fil d'exécution que le test unitaire.
- Le test unitaire ne sait faire que des vérifications.
- Les similis sont nécessairement des interfaces.

Comme dirait Linus Torvalds "Fais péter le code au lieu de causer !" ("Talk is cheap, show me the code.")

<<<
  @Test
  public void capture() throws InterruptedException {
    try( final Mockster mockster = new Mockster() ) {
      final Engine engine = mockster.mock( Engine.class ) ;
      final LongConsumer resultConsumer = l -> { } ;

      // What System Under Test could do.
      final Thread computationRunner = new Thread(
          () -> {
            LOGGER.info( "Sleeping a bit ..." ) ;
            Uninterruptibles.sleepUninterruptibly( 1, TimeUnit.SECONDS ) ;
            LOGGER.info( "Now we perform addition ..." ) ;
            engine.addAsync( 12, 2, resultConsumer ) ;
            LOGGER.info( "Addition complete." ) ;
          },
          "computation-runner"
      ) ;
      computationRunner.start() ;

      final LongConsumer captured ;
      LOGGER.info( "Verifying ..." ) ;
      engine.addAsync( exactly( 12 ), exactly( 2 ), captured = withCapture() ) ;
      LOGGER.info( "Verification complete." ) ;
      assertThat( captured ).isSameAs( resultConsumer ) ;

      computationRunner.join() ;
    }
  }
>>>

Et là ça marche.

On remarque la forte influence de JMockit dans certains choix de syntaxe, notamment le ``withCapture()``.

Bon il y a des petits trucs agaçants. Par exemple il faut dupliquer le type du simili, une fois dans la déclaration et une fois comme paramètre de ``mockster.mock(_)``. Et quand on a un paramètre magique comme ``withCapture()`` pas moyen de mélanger avec des litéraux, il faut utiliser ``exactly(_)`` pour ceux-là. Mais une fois qu'on l'a codé on voit que c'est inévitable. Le ``try()`` pour le Mockster est sympa mais on n'a pas la limitation de portée syntaxique, donc on finit par taper dans des méthodes statiques visibles de partout comme ``withCapture()``, qui échoueront s'il n'y a pas une instance de ``Mockster`` attachée au fil d'exécution en cours.

Oui on fait un peu de magie noire avec les fils d'exécution. Par exemple un appel de ``withCapture()`` va bloquer le fil d'exécution du test jusqu'à ce que la méthode du système à tester l'appelle de son propre fil d'exécution, mais restant bloquée jusqu'à ce que la méthode ``addAsync(_)`` autour du ``withCapture()`` se termine, ce qui implique l'exécution de toutes les méthodes "magiques" représentant des vérifications des arguments.

Autrement dit c'est facile de générer de l'interblocage si on n'a pas une exécution complètement séquentialisable du code à tester. Comment faire si on a un simili pour un client et un autre pour un serveur, et qu'ils s'exécutent concurremment, chacun avec ses fils d'exécution ? Réponse : on est mort. Le truc c'est d'avoir //un// fil d'exécution partagé par le client et le serveur, ce qui est possible avec l'``EventLoop`` de Netty. Je ne suis pas certain que les auteurs de Netty aient imaginé que l'``EventLoop`` partageable allait rendre possible des tests d'intégration séquentialisés mais dans tous les cas chapeau bas pour ces messieurs.

Si on veut voir ce que ça donne en vrai il y a les "tests d'intégration de Chiron"
qui utilisent un niveau d'abstraction par-dessus le ``Mockster`` : le ``ConnectorDrill`` qui instancie tous les composants spécifiés dans le test (similis, upend, downend, proxy HTTP ...).

Je ne vais pas rentrer dans tous les détails du Mockster, parce que les gens avec des besoins aussi pointus ont probablement déjà codé leur solution. En plus vu de loin le Mockster ressemble furieusement à un bloc ``Verifications`` de JMockit donc on peut se demander où est la nouveauté.

Elle se situe juste dans la façon de délimiter les appels au simili pour l'enregistrement-vérification, de ceux effectués par le système à tester. JMockit effectue cette séparation grâce à une utilisation astucieuse de l'initialiseur anonyme. Avec le Mockster la séparation se fait grâce au fil d'exécution en cours. Si le code est dans le fil d'exécution du test c'est qu'on vérifie, autrement c'est un appel à vérifier. Je n'ai pas trop regardé mais à ma connaissance le Mockster est le premier à faire cela. 

Bon c'est pas une grande révolution non plus mais ça me fait du bien d'en parler, après avoir transpiré comme un âne à essayer de faire fonctionner un JMockit qui n'est manifestement pas fait pour.


Henri Tremblay

unread,
Jul 1, 2018, 12:08:17 AM7/1/18
to tec...@googlegroups.com
C'est vrai qu'on avait besoin d'une autre librairie de mock :-)

Tu n'aurais pas pu pousser un patch à JMockit? Et sinon je cherche des contributeurs pour EasyMock :-) C'est comme JMockit mais moins lourds à la syntaxe (j'haîe les moustaches, les inner class et patata). J'haïe aussi les final un peu partout d'ailleurs. C'est du bruit.

Les frameworks de mock n'ont jamais été bon en multi-threading. J'ai eu souvent le problème avec EasyMock. Un peu différent de ton cas toutefois. La capture, c'est spécial. Mais souvent, la synchronisation d'Easymock et celle du l'objet réel n'est pas la même. Donc EasyMock risque de ne pas réagir de la même façon est donc le test ne marche pas ou ne teste pas correctement. Je n'ai à ce jour pas de solution.

--
Vous recevez ce message, car vous êtes abonné au groupe Google Groupes "techos".
Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+un...@googlegroups.com.
Pour envoyer un message à ce groupe, envoyez un e-mail à l'adresse tec...@googlegroups.com.
Visitez ce groupe à l'adresse https://groups.google.com/group/techos.
Pour obtenir davantage d'options, consultez la page https://groups.google.com/d/optout.

Laurent Caillette

unread,
Jul 1, 2018, 8:31:23 AM7/1/18
to tec...@googlegroups.com
Salut Henri,

J'ai regardé les dessous de JMockit mais c'est horriblement compliqué. Ce n'est pas un reproche, les fonctionnalités le justifient. Ça fait des années qu'il est question d'un paramètre ``timeout`` dans les ``Verifications`` mais l'auteur n'y tient pas trop, il trouve que ça encouragerait les gens à développer des tests orientés état alors qu'il veut qu'on écrive des tests orientés comportement. Ou l'inverse. Je trouve qu'il déconne complètement, son produit est le plus attirant parce que tu as l'impression que tu ne seras jamais coincé. Sauf que c'est pas vrai.

Les moustaches c'est vraiment pas le pire, au début Mockster essayait de reproduire les initialiseurs statiques de JMockit. Mais je me suis calmé parce que sans instrumenter la classe dérivée on ne sait pas quand se termine le code de l'initialiseur anonyme. Les ``final`` ça aurait du être le choix par défaut en Java, je les mets tout le temps et ça m'a sauvé la vie quelques fois. Le plus gros défaut de JMockit en utilisation normale, c'est le retour d'erreur, il te dit "tel appel n'a pas eu lieu" mais pas ce qui a eu lieu à la place. Pour ça Mockito est vraiment mieux. Mockster journalise tout, c'est bruyant mais au moins tu vois ce qui se passe. EasyMock j'ai essayé il y a très très longtemps, puis Mockito est sorti. C'est marrant la syntaxe de JMockit est pour moi un de ses points forts, tout est très homogène avec un minimum de redondances dans les déclarations. Regarde comment sont définis les fakes c'est grandiose.

Concernant la capture oui c'est spécial, et la capture de fonction de rappel encore plus. Je m'en tire parce que j'arrive à tout séquentialiser (merci l'``EventLoop``) mais peu de gens se donnent cette possibilité en Java (peut-être que c'est plus facile avec un modèle "acteur" comme celui d'Erlang, ça aiderait à comprendre son succès). 

Existe-t-il une demande pour un outil qui instrumenterait le planificateur des threads, afin de respecter des contraintes définies dans un test ? Par exemple "L'appel de méthode A s'effectue dans le thread X //et// l'appel B s'effectue dans le thread Y et leur ordre est indifférent et on veut X différent de Y. Et on veut que l'appel C dans un thread Z bloque jusqu'à l'achèvement de A." Le coup du "machin doit bloquer jusqu'à la fin de truc" c'est super-dur à tester aujourd'hui à moins de mettre des délais affreux. Ensuite il y a moyen de générer diverses combinaisons dans la planification des tâches, pour vérifier que les contraintes sont bien satisfaites. 

On a un produit apparenté avec "Jepsen"
qui effectue des tests d'intégrité sur un système distribué, en faisant tourner des machines virtuelles qu'il secoue vigoureusement à diverses reprises ("fuzzing"). Mais il n'y a rien qui fasse tout dans la même JVM. Cela dit je n'ai aucune idée sur la façon de formaliser les contraintes.




Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+unsubscribe@googlegroups.com.

Pour envoyer un message à ce groupe, envoyez un e-mail à l'adresse tec...@googlegroups.com.
Visitez ce groupe à l'adresse https://groups.google.com/group/techos.
Pour obtenir davantage d'options, consultez la page https://groups.google.com/d/optout.

--
Vous recevez ce message, car vous êtes abonné au groupe Google Groupes "techos".
Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+unsubscribe@googlegroups.com.

Henri Tremblay

unread,
Jul 1, 2018, 10:47:11 PM7/1/18
to tec...@googlegroups.com
Mockito a de très loin le lead actuellement pour les frameworks de mock. Je trouve ça dommage, car il y a des gros défauts. En particulier la syntaxe qui est un coup à droite, un coup à gauche en fonction des spy, void et patata. L'autre problème, et ça je ne comprend pas pourquoi ça ne rend pas tout le monde fou c'est que Mockito va dire NullPointerException quand EasyMock (et JMockit) dit "Unexpected call bla bla bla".

Par contre, (mais je n'ai pas regardé ça fait longtemps), JMockit je l'ai trouvé beaucoup trop rempli de boilerplate.

Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+un...@googlegroups.com.

Pour envoyer un message à ce groupe, envoyez un e-mail à l'adresse tec...@googlegroups.com.
Visitez ce groupe à l'adresse https://groups.google.com/group/techos.
Pour obtenir davantage d'options, consultez la page https://groups.google.com/d/optout.

--
Vous recevez ce message, car vous êtes abonné au groupe Google Groupes "techos".
Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+un...@googlegroups.com.

Pour envoyer un message à ce groupe, envoyez un e-mail à l'adresse tec...@googlegroups.com.
Visitez ce groupe à l'adresse https://groups.google.com/group/techos.
Pour obtenir davantage d'options, consultez la page https://groups.google.com/d/optout.

--
Vous recevez ce message, car vous êtes abonné au groupe Google Groupes "techos".
Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+un...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages