Mockito prende in giro la classe finale locale ma fallisce a Jenkins


11

Ho scritto alcuni test unitari per un metodo statico. Il metodo statico accetta solo un argomento. Il tipo di argomento è una classe finale. In termini di codice:

public class Utility {

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

public final class Customer {
   // class definition
}

Così, per la Utilityclasse che ho creato una classe di test UtilityTestsin cui ho scritto le prove per questo metodo, getName. Il framework di unit test è TestNG e la libreria di simulazione utilizzata Mockito. Quindi un test tipico ha la seguente struttura:

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

Qual è il problema ?

Mentre i test vengono eseguiti correttamente localmente, all'interno di IntelliJ, falliscono su Jenkins (quando spingo il mio codice nel ramo remoto, viene avviata una build e i test unitari vengono eseguiti alla fine). Il messaggio di errore è simile al seguente:

org.mockito.exceptions.base.MockitoException: Impossibile deridere / spia classe com.packagename Il cliente Mockito non può deridere / spia perché: - classe finale

Cosa ho provato?

Ho cercato un po ', per trovare una soluzione ma non ce l'ho fatta. Noto qui che non mi è permesso cambiare il fatto che Customerè una classe finale . Inoltre, vorrei, se possibile, non cambiare affatto il suo design (ad esempio, la creazione di un'interfaccia, che potesse contenere i metodi che desidero deridere e affermare che la classe Customer implementa quell'interfaccia, come giustamente sottolineato da Jose nel suo commento). La cosa che ho provato è la seconda opzione menzionata al mockito-final . Nonostante il fatto che ciò abbia risolto il problema, ha frenato altri test unitari :(, che non possono essere risolti in nessun modo apparente.

Domande

Quindi, ecco le due domande che ho:

  1. Come è possibile in primo luogo? Il test non dovrebbe fallire sia a livello locale che a Jenkins?
  2. Come può essere risolto in base ai vincoli che ho menzionato sopra?

Grazie in anticipo per qualsiasi aiuto.


1
La mia ipotesi sarebbe che la enable finalconfigurazione funzioni nel tuo spazio di lavoro, ma una volta eseguita Jenkinsnon è in grado di trovare questo file. Controlla dove Jenkinssta cercando il file e se è effettivamente lì o no.
secondo

Quest'altro thread spiega come abilitare il derisione della classe finale in Mockito 2, aggiungendo un file di configurazione di mockito nella directory delle risorse: stackoverflow.com/questions/14292863/…
Jose Tepedino

3
Sarebbe possibile, nel codice con cui hai a che fare, estrarre un'interfaccia dalla classe Customer, dire ICustomer, e usarla nella classe Utility? Quindi potresti deridere quell'interfaccia invece della classe finale concreta
Jose Tepedino

@JoseTepedino Questo è un punto valido. Ha completamente senso ed è sicuramente un modo elegante per superare questo problema. Tuttavia mi chiedo se c'è un altro modo e, cosa più importante, voglio capire perché l'attuale approccio ha successo localmente e fallisce in Jenkins.
Christos,

1
C'è Customerqualche logica in esso, o è solo una classe di dati stupida? Se è solo un gruppo di campi con getter e setter, puoi semplicemente istanziarlo.
Willis Blackburn,

Risposte:


2

Un approccio alternativo sarebbe quello di utilizzare il modello "metodo per classificare".

  1. Sposta i metodi fuori dalla classe cliente in un'altra classe / classi, ad esempio CustomerSomething per esempio / CustomerFinances (o qualunque sia la sua responsabilità).
  2. Aggiungi un costruttore al cliente.
  3. Ora non è necessario prendere in giro il cliente, solo la classe CustomerSomething! Potrebbe non essere necessario prenderlo in giro neanche se non ha dipendenze esterne.

Ecco un buon blog sull'argomento: https://simpleprogrammer.com/back-to-basics-mock-eliminating-patterns/


1
Grazie per la tua risposta (+1). Ho trovato un modo per risolverlo (risposta alla seconda domanda). Tuttavia, il motivo per cui i test falliscono all'interno di IntelliJ non mi è ancora chiaro. Inoltre, non riesco più a riprodurlo (il fallimento all'interno di IntelliJ), il che è totalmente strano.
Christos

1

Come è possibile in primo luogo? Il test non dovrebbe fallire sia a livello locale che a Jenkins?

È ovviamente una specie di specificità ambientale. L'unica domanda è: come determinare la causa della differenza.

Ti suggerirei di verificare il org.mockito.internal.util.MockUtil#typeMockabilityOfmetodo e confrontare, cosa mockMakerviene effettivamente utilizzato in entrambi gli ambienti e perché.

Se mockMakerè lo stesso - confronta le classi caricate IDE-Clientvs Jenkins-Client- hanno qualche differenza sul tempo di esecuzione del test.

Come può essere risolto in base ai vincoli che ho menzionato sopra?

Il seguente codice è scritto in presupposto di OpenJDK 12 e Mockito 2.28.2, ma credo che tu possa adattarlo a qualsiasi versione effettivamente utilizzata.

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

Con una regola separata per le derisioni in linea:

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

Grazie per la tua risposta (+1). Ho trovato un modo per risolverlo (risposta alla seconda domanda). Tuttavia, il motivo per cui i test falliscono all'interno di IntelliJ non mi è ancora chiaro. Inoltre, non riesco più a riprodurlo (il fallimento all'interno di IntelliJ), il che è totalmente strano.
Christos

1

Assicurati di eseguire il test con gli stessi argomenti. Controlla se le configurazioni di esecuzione di Intellij corrispondono ai jenkins. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html . Puoi provare a eseguire il test sul computer locale con gli stessi argomenti di jenkins (dal terminale), se fallirà ciò significa che il problema è negli argomenti


Il file org.mockito.plugins.MockMakeresiste anche nella macchina jenkins. Uso la stessa JVM nelle macchine bot. Controllerò i 3 che hai indicato. Grazie
Christos il

Ho provato a eseguire il test tramite console, usando il comando usato in Jenkins. Non riescono con lo stesso messaggio di errore esatto. Quindi qualcosa di strano accade all'interno di IntelliJ.
Christos,

Dai un'occhiata a .idea / workspace.xml alla tua configration di esecuzione, è all'interno di un tag <component>. Dopodiché puoi imparare come trasformare quel file XML in comando bash
Link182

Puoi mostrare il comando jenkins terminal che viene utilizzato per eseguire i test? Puoi anche dirmi quale gestore di pacchetti usi?
Link182

Come strumento di costruzione, utilizzo Gradle.
Christos,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.