Mockito se burla localmente de la clase final pero falla en Jenkins

11

He escrito algunas pruebas unitarias para un método estático. El método estático toma solo un argumento. El tipo de argumento es una clase final. En términos de código:

public class Utility {

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

public final class Customer {
   // class definition
}

Así que para la Utilityclase I se ha creado una clase de prueba UtilityTestsen la que he escrito ensayos para este método, getName. El marco de prueba de la unidad es TestNG y la biblioteca de burla que se usa es Mockito. Entonces, una prueba típica tiene la siguiente estructura:

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

Cuál es el problema ?

Mientras que las pruebas se ejecutan con éxito localmente, dentro de IntelliJ, fallan en Jenkins (cuando inserto mi código en la rama remota, se activa una compilación y las pruebas unitarias se ejecutan al final). El mensaje de error es algo como lo siguiente:

org.mockito.exceptions.base.MockitoException: No se puede burlar / espiar clase com.packagename.Customer Mockito no se puede burlar / espiar porque: - clase final

Lo que probé?

Busqué un poco para encontrar una solución pero no la encontré. Tomo nota aquí que estoy no autorizado a cambiar el hecho de que Customeres una última clase. Además de esto, me gustaría, si es posible, no cambiar su diseño (por ejemplo, crear una interfaz, que contenga los métodos de los que quiero burlarme y afirmar que la clase Cliente implementa esa interfaz, como señaló correctamente José en su comentario). Lo que probé es la segunda opción mencionada en mockito-final . A pesar de que esto solucionó el problema, frenó algunas otras pruebas unitarias :(, que no se pueden solucionar de ninguna manera aparente.

Preguntas

Aquí están las dos preguntas que tengo:

  1. ¿Cómo es eso posible en primer lugar? ¿No debería fallar la prueba tanto a nivel local como en Jenkins?
  2. ¿Cómo se puede solucionar esto en función de las restricciones que mencioné anteriormente?

Gracias de antemano por cualquier ayuda.

Christos
fuente
1
Supongo que la enable finalconfiguración funciona en su espacio de trabajo, pero cuando se ejecuta Jenkinsno puede encontrar este archivo. Verifique dónde Jenkinsestá buscando el archivo y si realmente está allí o no.
segundo
Este otro hilo explica cómo habilitar la burla de la clase final en Mockito 2, agregando un archivo de configuración de mockito en el directorio de recursos: stackoverflow.com/questions/14292863/…
Jose Tepedino
3
¿Sería posible, en el código con el que está tratando, extraer una interfaz de la clase Cliente, digamos ICustomer, y usarla en la clase Utilidad? Entonces podrías burlarte de esa interfaz en lugar de la clase final concreta
José Tepedino, el
@JoseTepedino Este es un punto válido. Tiene sentido totalmente y definitivamente es una forma elegante de superar este problema. Sin embargo, me pregunto si hay otra forma y, lo que es más importante, quiero entender por qué el enfoque actual tiene éxito localmente y falla en Jenkins.
Christos
1
¿ CustomerTiene alguna lógica o es solo una clase de datos tontos? Si se trata solo de un conjunto de campos con captadores y establecedores, puede crear una instancia.
Willis Blackburn, el

Respuestas:

2

Un enfoque alternativo sería utilizar el patrón 'método para clasificar'.

  1. Mueva los métodos fuera de la clase de cliente a otra clase / clases, diga CustomerSomething eg / CustomerFinances (o lo que sea responsabilidad).
  2. Agregar un constructor al cliente.
  3. ¡Ahora no necesita burlarse del Cliente, solo la clase CustomerSomething! Es posible que tampoco necesite burlarse de eso si no tiene dependencias externas.

Aquí hay un buen blog sobre el tema: https://simpleprogrammer.com/back-to-basics-mock-eliminating-patterns/

Johnny Alpha
fuente
1
Gracias por tu respuesta (+1). Encontré una forma de solucionarlo (respuesta a la segunda pregunta). Sin embargo, la razón por la cual las pruebas fallan dentro de IntelliJ todavía no está clara para mí. Además, ya no puedo reproducirlo (la falla dentro del IntelliJ), lo cual es totalmente extraño.
Christos
1

¿Cómo es eso posible en primer lugar? ¿No debería fallar la prueba tanto a nivel local como en Jenkins?

Obviamente es una especie de especificidad ambiental. La única pregunta es: cómo determinar la causa de la diferencia.

Le sugiero que verifique el org.mockito.internal.util.MockUtil#typeMockabilityOfmétodo y compare, qué mockMakerse usa realmente en ambos entornos y por qué.

Si mockMakeres lo mismo, compare las clases cargadas IDE-Clientvs Jenkins-Client, ¿tienen alguna diferencia en el momento de la ejecución de la prueba?

¿Cómo se puede solucionar esto en función de las restricciones que mencioné anteriormente?

El siguiente código está escrito en el supuesto de OpenJDK 12 y Mockito 2.28.2, pero creo que puede ajustarlo a cualquier versión realmente utilizada.

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 regla separada para simulacros en línea:

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
fuente
Gracias por tu respuesta (+1). Encontré una forma de solucionarlo (respuesta a la segunda pregunta). Sin embargo, la razón por la cual las pruebas fallan dentro de IntelliJ todavía no está clara para mí. Además, ya no puedo reproducirlo (la falla dentro del IntelliJ), lo cual es totalmente extraño.
Christos
1

Asegúrese de ejecutar la prueba con los mismos argumentos. Compruebe si sus configuraciones de ejecución inteligente coinciden con las jenkins. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html . Puede intentar ejecutar la prueba en la máquina local con los mismos argumentos que en jenkins (desde la terminal), si falla, eso significa que el problema está en los argumentos

Enlace182
fuente
El archivo org.mockito.plugins.MockMakertambién existe en la máquina jenkins. Yo uso la misma JVM en máquinas bot. Comprobaré los 3 que señaló. Gracias
Christos
Traté de ejecutar la prueba a través de la consola, usando el comando utilizado en Jenkins. Fallan con el mismo mensaje de error exacto. Entonces algo extraño sucede dentro del IntelliJ.
Christos
Eche un vistazo a .idea / workspace.xml en su configuración de ejecución, está dentro de una etiqueta <componente>. Después de eso, puede aprender cómo transformar ese xml en comando bash
Link182
¿Puedes mostrar el comando del terminal jenkins que se usa para ejecutar pruebas? ¿También me puede decir qué administrador de paquetes utiliza?
Link182
Como herramienta de compilación, uso Gradle.
Christos