Mockito se moque de la classe finale locale mais échoue à Jenkins

11

J'ai écrit quelques tests unitaires pour une méthode statique. La méthode statique ne prend qu'un seul argument. Le type de l'argument est une classe finale. En termes de code:

public class Utility {

   public static Optional<String> getName(Customer customer) {
       // method's body.
   }
}

public final class Customer {
   // class definition
}

Donc , pour la Utilityclasse que j'ai créé une classe de test UtilityTestsdans lequel j'ai des tests écrits pour cette méthode, getName. Le framework de tests unitaires est TestNG et la bibliothèque de simulation utilisée est Mockito. Un test typique a donc la structure suivante:

public class UtilityTests {

   @Test
   public void getNameTest() {
     // Arrange
     Customer customerMock = Mockito.mock(Customer.class);
     Mockito.when(...).thenReturn(...);

     // Act
     Optional<String> name = Utility.getName(customerMock);

     // Assert
     Assert.assertTrue(...);
   }
}

Quel est le problème ?

Alors que les tests s'exécutent avec succès localement, à l'intérieur d'IntelliJ, ils échouent sur Jenkins (lorsque je pousse mon code dans la branche distante, une génération est déclenchée et des tests unitaires s'exécutent à la fin). Le message d'erreur est comme suit:

org.mockito.exceptions.base.MockitoException: ne peut pas se moquer / espionner classe com.packagename.Customer Mockito ne peut pas se moquer / espionner parce que: - classe finale

Qu'est-ce que j'ai essayé?

J'ai cherché un peu, pour trouver une solution mais je n'y suis pas arrivée. Je note ici que je ne suis pas autorisé à changer le fait qu'il Customers'agit d'une classe finale . En plus de cela, j'aimerais si possible ne pas changer du tout sa conception (par exemple, créer une interface, qui contiendrait les méthodes que je veux simuler et déclarer que la classe Customer implémente cette interface, comme Jose l'a correctement souligné dans son commentaire). La chose que j'ai essayée est la deuxième option mentionnée au mockito-final . Malgré le fait que cela ait résolu le problème, cela freine certains autres tests unitaires :(, qui ne peuvent pas être résolus de manière apparente.

Des questions

Voici donc les deux questions que j'ai:

  1. Comment est-ce possible en premier lieu? Le test ne devrait-il pas échouer localement et à Jenkins?
  2. Comment cela peut-il être corrigé en fonction des contraintes que j'ai mentionnées ci-dessus?

Merci d'avance pour votre aide.

Christos
la source
1
Je suppose que la enable finalconfiguration fonctionne dans votre espace de travail, mais lorsqu'elle est exécutée sur Jenkinsson impossible de trouver ce fichier. Vérifiez où Jenkinsle fichier est recherché et s'il est réellement présent ou non.
deuxième
Cet autre fil explique comment activer la simulation de classe finale dans Mockito 2, en ajoutant un fichier de configuration de mockito dans le répertoire des ressources: stackoverflow.com/questions/14292863/…
Jose Tepedino
3
Serait-il possible, dans le code que vous traitez, d'extraire une interface de la classe Customer, par exemple ICustomer, et de l'utiliser dans la classe Utility? Ensuite, vous pouvez vous moquer de cette interface au lieu de la classe finale concrète
Jose Tepedino
@JoseTepedino Ceci est un point valide. Cela a du sens et c'est certainement une façon élégante de surmonter ce problème. Cependant, je me demande s'il existe un autre moyen et, plus important encore, je veux comprendre pourquoi l'approche actuelle réussit localement et échoue à Jenkins.
Christos
1
Y Customera- t -il une logique ou s'agit-il simplement d'une classe de données stupide? Si c'est juste un tas de champs avec des getters et des setters, alors vous pouvez simplement l'instancier.
Willis Blackburn

Réponses:

2

Une autre approche consisterait à utiliser le modèle «méthode pour classer».

  1. Déplacez les méthodes de la classe client vers une autre classe / classes, par exemple CustomerSomething, par exemple / CustomerFinances (ou quelle que soit sa responsabilité).
  2. Ajoutez un constructeur au client.
  3. Maintenant, vous n'avez plus besoin de vous moquer du client, juste la classe CustomerSomething! Vous n'aurez peut-être pas besoin de vous en moquer non plus s'il n'a pas de dépendances externes.

Voici un bon blog sur le sujet: https://simpleprogrammer.com/back-to-basics-mock-élimination-patterns/

Johnny Alpha
la source
1
Merci pour votre réponse (+1). J'ai trouvé un moyen de le réparer (réponse à la deuxième question). Cependant, la raison pour laquelle les tests échouent à l'intérieur d'IntelliJ n'est toujours pas claire pour moi. De plus, je ne peux plus le reproduire (l'échec à l'intérieur de l'IntelliJ), ce qui est totalement bizarre.
Christos
1

Comment est-ce possible en premier lieu? Le test ne devrait-il pas échouer localement et à Jenkins?

C'est évidemment une sorte de spécificités env. La seule question est - comment déterminer la cause de la différence.

Je vous suggère de vérifier la org.mockito.internal.util.MockUtil#typeMockabilityOfméthode et de comparer ce qui mockMakerest réellement utilisé dans les deux environnements et pourquoi.

Si mockMakerc'est la même chose - comparer les classes chargées IDE-Clientvs Jenkins-Client- ont-ils une différence sur le moment de l'exécution du test.

Comment cela peut-il être corrigé en fonction des contraintes que j'ai mentionnées ci-dessus?

Le code suivant est écrit en supposant OpenJDK 12 et Mockito 2.28.2, mais je pense que vous pouvez l'ajuster à n'importe quelle version réellement utilisée.

public class UtilityTest {    
    @Rule
    public InlineMocksRule inlineMocksRule = new InlineMocksRule();

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testFinalClass() {
        // Given
        String testName = "Ainz Ooal Gown";
        Client client = Mockito.mock(Client.class);
        Mockito.when(client.getName()).thenReturn(testName);

        // When
        String name = Utility.getName(client).orElseThrow();

        // Then
        assertEquals(testName, name);
    }

    static final class Client {
        final String getName() {
            return "text";
        }
    }

    static final class Utility {
        static Optional<String> getName(Client client) {
            return Optional.ofNullable(client).map(Client::getName);
        }
    }    
}

Avec une règle distincte pour les simulateurs en ligne:

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.util.MockUtil;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class InlineMocksRule implements TestRule {
    private static Field MOCK_MAKER_FIELD;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
            VarHandle modifiers = lookup.findVarHandle(Field.class, "modifiers", int.class);

            MOCK_MAKER_FIELD = MockUtil.class.getDeclaredField("mockMaker");
            MOCK_MAKER_FIELD.setAccessible(true);

            int mods = MOCK_MAKER_FIELD.getModifiers();
            if (Modifier.isFinal(mods)) {
                modifiers.set(MOCK_MAKER_FIELD, mods & ~Modifier.FINAL);
            }
        } catch (IllegalAccessException | NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Object oldMaker = MOCK_MAKER_FIELD.get(null);
                MOCK_MAKER_FIELD.set(null, Plugins.getPlugins().getInlineMockMaker());
                try {
                    base.evaluate();
                } finally {
                    MOCK_MAKER_FIELD.set(null, oldMaker);
                }
            }
        };
    }
}
Ursa
la source
Merci pour votre réponse (+1). J'ai trouvé un moyen de le réparer (réponse à la deuxième question). Cependant, la raison pour laquelle les tests échouent à l'intérieur d'IntelliJ n'est toujours pas claire pour moi. De plus, je ne peux plus le reproduire (l'échec à l'intérieur de l'IntelliJ), ce qui est totalement bizarre.
Christos
1

Assurez-vous d'exécuter le test avec les mêmes arguments. Vérifiez si vos configurations d'exécution intellij correspondent aux jenkins. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html . Vous pouvez essayer d'exécuter le test sur la machine locale avec les mêmes arguments que sur jenkins (depuis le terminal), si cela échoue, cela signifie que le problème est dans les arguments

Link182
la source
Le fichier org.mockito.plugins.MockMakerexiste également dans la machine jenkins. J'utilise la même JVM dans les machines bot. Je vais vérifier les 3 que vous avez indiqués. Merci
Christos
J'ai essayé d'exécuter le test via la console, en utilisant la commande utilisée dans Jenkins. Ils échouent avec le même message d'erreur exact. Donc, quelque chose d'étrange se produit à l'intérieur de l'IntelliJ.
Christos
Jetez un œil à .idea / workspace.xml lors de votre configuration de migration, il se trouve à l'intérieur d'une balise <component>. Après cela, vous pouvez apprendre à transformer ce xml en commande bash
Link182
Pouvez-vous montrer la commande jenkins terminal qui est utilisée pour exécuter des tests? Pouvez-vous également me dire quel gestionnaire de paquets utilisez-vous?
Link182
En tant qu'outil de construction, j'utilise Gradle.
Christos