Mockito verspottet die lokale Endklasse, scheitert aber in Jenkins

11

Ich habe einige Unit-Tests für eine statische Methode geschrieben. Die statische Methode akzeptiert nur ein Argument. Der Typ des Arguments ist eine letzte Klasse. In Bezug auf Code:

public class Utility {

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

public final class Customer {
   // class definition
}

Also habe Utilityich für die Klasse eine Testklasse erstellt, UtilityTestsin der ich Tests für diese Methode geschrieben habe getName. Das Unit-Test-Framework ist TestNG und die verwendete Mocking-Bibliothek ist Mockito. Ein typischer Test hat also folgende Struktur:

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(...);
   }
}

Was ist das Problem ?

Während die Tests lokal erfolgreich ausgeführt werden, schlagen sie in IntelliJ auf Jenkins fehl (wenn ich meinen Code in den Remote-Zweig schiebe, wird ein Build ausgelöst und am Ende werden Komponententests ausgeführt). Die Fehlermeldung lautet wie folgt:

org.mockito.exceptions.base.MockitoException: Klasse / Spion-Klasse kann nicht verspottet werden com.packagename.Customer Mockito kann nicht verspotten / spionieren, weil: - letzte Klasse

Was habe ich versucht?

Ich habe ein bisschen gesucht, um eine Lösung zu finden, aber ich habe es nicht geschafft. Ich stelle hier fest, dass ich die Tatsache, dass es sich um eine Abschlussklasse handelt, nicht ändern darf . Darüber hinaus möchte ich, wenn möglich, das Design überhaupt nicht ändern (z. B. eine Schnittstelle erstellen, die die Methoden enthält, die ich verspotten möchte, und angeben, dass die Customer-Klasse diese Schnittstelle implementiert, wie Jose in seinem Bericht korrekt ausgeführt hat Kommentar). Das, was ich versucht habe, ist die zweite Option, die im Mockito-Finale erwähnt wurde . Trotz der Tatsache, dass dies das Problem behoben hat, bremst es einige andere Unit-Tests :(, die nicht auf offensichtliche Weise behoben werden können.Customer

Fragen

Hier sind die beiden Fragen, die ich habe:

  1. Wie ist das überhaupt möglich? Sollte der Test nicht sowohl lokal als auch in Jenkins fehlschlagen?
  2. Wie kann dies aufgrund der oben genannten Einschränkungen behoben werden?

Vielen Dank im Voraus für jede Hilfe.

Christos
quelle
1
Ich vermute, dass die enable finalKonfiguration in Ihrem Arbeitsbereich funktioniert, aber wenn sie ausgeführt wird Jenkins, kann sie diese Datei nicht finden. Überprüfen Sie, wo Jenkinsnach der Datei gesucht wird und ob sie tatsächlich vorhanden ist oder nicht.
Zweiter
Dieser andere Thread erklärt, wie das Verspotten der letzten Klasse in Mockito 2 aktiviert wird, indem eine Mockito-Konfigurationsdatei im Ressourcenverzeichnis hinzugefügt wird
Jose Tepedino
3
Wäre es möglich, in dem Code, mit dem Sie sich befassen, eine Schnittstelle aus der Kundenklasse zu extrahieren, z. B. ICustomer, und sie in der Utility-Klasse zu verwenden? Dann könnten Sie diese Schnittstelle anstelle der konkreten Abschlussklasse verspotten
Jose Tepedino
@JoseTepedino Dies ist ein gültiger Punkt. Es macht absolut Sinn und ist definitiv ein eleganter Weg, um dieses Problem zu überwinden. Ich frage mich jedoch, ob es einen anderen Weg gibt, und was noch wichtiger ist, ich möchte verstehen, warum der derzeitige Ansatz lokal erfolgreich ist und in Jenkins fehlschlägt.
Christos
1
Hat Customeres irgendeine Logik oder ist es nur eine dumme Datenklasse? Wenn es sich nur um eine Reihe von Feldern mit Getter und Setter handelt, können Sie es einfach instanziieren.
Willis Blackburn

Antworten:

2

Ein alternativer Ansatz wäre die Verwendung des Musters "Methode zum Klassifizieren".

  1. Verschieben Sie die Methoden aus der Kundenklasse in eine andere Klasse / Klassen, z. B. CustomerSomething, z. B. / CustomerFinances (oder was auch immer dafür verantwortlich ist).
  2. Fügen Sie dem Kunden einen Konstruktor hinzu.
  3. Jetzt müssen Sie sich nicht mehr über den Kunden lustig machen, sondern nur noch über die CustomerSomething-Klasse! Möglicherweise müssen Sie sich auch nicht darüber lustig machen, wenn keine externen Abhängigkeiten bestehen.

Hier ist ein guter Blog zum Thema: https://simpleprogrammer.com/back-to-basics-mock-eliminating-patterns/

Johnny Alpha
quelle
1
Danke für deine Antwort (+1). Ich habe einen Weg gefunden, dies zu beheben (Antwort auf die zweite Frage). Der Grund, warum die Tests in IntelliJ fehlschlagen, ist mir jedoch immer noch nicht klar. Außerdem kann ich es nicht mehr reproduzieren (der Fehler im IntelliJ), was total seltsam ist.
Christos
1

Wie ist das überhaupt möglich? Sollte der Test nicht sowohl lokal als auch in Jenkins fehlschlagen?

Es ist offensichtlich eine Art Umwelt-Besonderheiten. Die Frage ist nur, wie die Ursache des Unterschieds ermittelt werden kann.

Ich würde Ihnen empfehlen, die org.mockito.internal.util.MockUtil#typeMockabilityOfMethode zu überprüfen und zu vergleichen, was mockMakerin beiden Umgebungen tatsächlich verwendet wird und warum.

Wenn mockMakerdas gleiche ist - vergleichen geladene Klassen IDE-Clientvs Jenkins-Client- haben sie einen Unterschied zu der Zeit der Testausführung.

Wie kann dies aufgrund der oben genannten Einschränkungen behoben werden?

Der folgende Code wurde unter der Annahme von OpenJDK 12 und Mockito 2.28.2 geschrieben, aber ich glaube, Sie können ihn an jede tatsächlich verwendete Version anpassen.

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);
        }
    }    
}

Mit einer separaten Regel für Inline-Mocks:

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
quelle
Danke für deine Antwort (+1). Ich habe einen Weg gefunden, dies zu beheben (Antwort auf die zweite Frage). Der Grund, warum die Tests in IntelliJ fehlschlagen, ist mir jedoch immer noch nicht klar. Außerdem kann ich es nicht mehr reproduzieren (der Fehler im IntelliJ), was total seltsam ist.
Christos
1

Stellen Sie sicher, dass Sie den Test mit denselben Argumenten ausführen. Überprüfen Sie, ob Ihre Intellij-Laufkonfigurationen mit den Jenkins übereinstimmen. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html . Sie können versuchen, den Test auf einem lokalen Computer mit denselben Argumenten wie auf Jenkins (vom Terminal) auszuführen. Wenn dies fehlschlägt, liegt das Problem in Argumenten

Link182
quelle
Die Datei org.mockito.plugins.MockMakerexistiert auch in Jenkins Maschine. Ich benutze die gleiche JVM in Bot-Maschinen. Ich werde die 3 überprüfen, auf die Sie hingewiesen haben. Danke
Christos
Ich habe versucht, den Test über die Konsole mit dem in Jenkins verwendeten Befehl auszuführen. Sie schlagen mit genau der gleichen Fehlermeldung fehl. Im IntelliJ passiert also etwas Seltsames.
Christos
Schauen Sie sich .idea / workspace.xml an, um herauszufinden, ob es sich um ein <component> -Tag handelt. Danach können Sie lernen, wie Sie diese XML in Bash-Befehl
umwandeln
Können Sie den Befehl jenkins terminal anzeigen, mit dem Tests ausgeführt werden? Können Sie mir auch sagen, welchen Paketmanager Sie verwenden?
Link182
Als Build-Tool verwende ich Gradle.
Christos