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.