Burlándose de métodos estáticos con Mockito


374

He escrito una fábrica para producir java.sql.Connectionobjetos:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return DriverManager.getConnection(...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Me gustaría validar los parámetros pasados DriverManager.getConnection, pero no sé cómo burlarme de un método estático. Estoy usando JUnit 4 y Mockito para mis casos de prueba. ¿Hay una buena manera de burlarse / verificar este caso de uso específico?



55
No puede con Mockito por el desing :)
MariuszS

25
@MariuszS No es por diseño que Mockito (o EasyMock, o jMock) no admite staticmétodos de burla , sino por accidente . Esta limitación (junto con la falta de soporte para burlarse de finalclases / métodos, u newobjetos -ed) es una consecuencia natural (pero no intencional) del enfoque empleado para implementar la burla, donde se crean dinámicamente nuevas clases que implementan / extienden el tipo que se burlará; otras bibliotecas burlonas usan otros enfoques que evitan estas limitaciones. Esto también sucedió en el mundo .NET.
Rogério

2
@ Rogério Gracias por la explicación. github.com/mockito/mockito/wiki/FAQ ¿Puedo burlarme de los métodos estáticos? No. Mockito prefiere la orientación del objeto y la inyección de dependencia sobre el código de procedimiento estático que es difícil de entender y cambiar. También hay un diseño detrás de esta limitación :)
MariuszS

17
@MariuszS Leí que como un intento de descartar casos de uso legítimos en lugar de admitir que la herramienta tiene limitaciones que no se pueden eliminar (fácilmente) y sin proporcionar ninguna justificación razonada. Por cierto, aquí hay una discusión para el punto de vista opuesto, con referencias.
Rogério

Respuestas:


350

Use PowerMockito encima de Mockito.

Código de ejemplo:

@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {

    @Test
    public void shouldVerifyParameters() throws Exception {

        //given
        PowerMockito.mockStatic(DriverManager.class);
        BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);

        //when
        sut.execute(); // System Under Test (sut)

        //then
        PowerMockito.verifyStatic();
        DriverManager.getConnection(...);

    }

Más información:


44
Si bien esto funciona en teoría, tener dificultades en la práctica ...
Naftuli Kay

38
Desafortunadamente, la gran desventaja de esto es la necesidad de PowerMockRunner.
Innokenty

18
sut.execute ()? ¿Medio?
TejjD

44
Sistema bajo prueba, la clase que requiere simulacro de DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
FYI, si ya estás usando JUnit4 puedes hacerlo @RunWith(PowerMockRunner.class)y debajo de eso @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations

71

La estrategia típica para esquivar métodos estáticos que no tiene forma de evitar es crear objetos envueltos y usar los objetos envolventes.

Los objetos de envoltura se convierten en fachadas a las clases estáticas reales, y no las prueba.

Un objeto contenedor podría ser algo así como

public class Slf4jMdcWrapper {
    public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();

    public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
        return MDC.getWhateverIWant();
    }
}

Finalmente, su clase bajo prueba puede usar este objeto singleton al, por ejemplo, tener un constructor predeterminado para uso en la vida real:

public class SomeClassUnderTest {
    final Slf4jMdcWrapper myMockableObject;

    /** constructor used by CDI or whatever real life use case */
    public myClassUnderTestContructor() {
        this.myMockableObject = Slf4jMdcWrapper.SINGLETON;
    }

    /** constructor used in tests*/
    myClassUnderTestContructor(Slf4jMdcWrapper myMock) {
        this.myMockableObject = myMock;
    }
}

Y aquí tiene una clase que se puede probar fácilmente, porque no usa directamente una clase con métodos estáticos.

Si está utilizando CDI y puede hacer uso de la anotación @Inject, entonces es aún más fácil. Simplemente haga su Wrapper bean @ApplicationScoped, inyecte esa cosa como colaborador (ni siquiera necesita constructores desordenados para las pruebas) y continúe con la burla.


3
Creé una herramienta para generar automáticamente interfaces "mixin" de Java 8 que envuelven llamadas estáticas: github.com/aro-tech/interface-it Los mixins generados se pueden burlar como cualquier otra interfaz, o si su clase bajo prueba "implementa" el interfaz puede anular cualquiera de sus métodos en una subclase para la prueba.
aro_tech

25

Tuve un problema similar. La respuesta aceptada no funcionó para mí, hasta que hice el cambio: de @PrepareForTest(TheClassThatContainsStaticMethod.class)acuerdo con la documentación de PowerMock para mockStatic .

Y no tengo que usar BDDMockito.

Mi clase:

public class SmokeRouteBuilder {
    public static String smokeMessageId() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("Exception occurred while fetching localhost address", e);
            return UUID.randomUUID().toString();
        }
    }
}

Mi clase de prueba:

@RunWith(PowerMockRunner.class)
@PrepareForTest(SmokeRouteBuilder.class)
public class SmokeRouteBuilderTest {
    @Test
    public void testSmokeMessageId_exception() throws UnknownHostException {
        UUID id = UUID.randomUUID();

        mockStatic(InetAddress.class);
        mockStatic(UUID.class);
        when(InetAddress.getLocalHost()).thenThrow(UnknownHostException.class);
        when(UUID.randomUUID()).thenReturn(id);

        assertEquals(id.toString(), SmokeRouteBuilder.smokeMessageId());
    }
}

No es capaz de descubrir? .MockStatic y? .When actualmente con JUnit 4
Teddy

PowerMock.mockStatic & Mockito.
Teddy

Para cualquiera que vea esto más tarde, para mí tuve que escribir PowerMockito.mockStatic (StaticClass.class);
thinkereer

Debe incluir el artefacto powermock-api-mockito maven.
PeterS

23

Como se mencionó anteriormente, no puede burlarse de los métodos estáticos con mockito.

Si cambiar su marco de prueba no es una opción, puede hacer lo siguiente:

Cree una interfaz para DriverManager, simule esta interfaz, inyecte a través de algún tipo de inyección de dependencia y verifique en esa simulación.


7

Observación: cuando llama al método estático dentro de una entidad estática, debe cambiar la clase en @PrepareForTest.

Por ejemplo:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

Para el código anterior si necesita burlarse de la clase MessageDigest, use

@PrepareForTest(MessageDigest.class)

Mientras que si tienes algo como a continuación:

public class CustomObjectRule {

    object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
             .digest(message.getBytes(ENCODING)));

}

entonces, deberías preparar la clase en la que reside este código.

@PrepareForTest(CustomObjectRule.class)

Y luego burlarse del método:

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());

Estaba golpeando mi cabeza contra la pared tratando de entender por qué mi clase estática no se estaba burlando. Se podría pensar que en todos los tutoriales en las redes sociales, ONE habría ido más allá del caso de uso básico.
SoftwareSavant

6

Puedes hacerlo con un poco de refactorización:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return _getConnection(...some params...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    //method to forward parameters, enabling mocking, extension, etc
    Connection _getConnection(...some params...) throws SQLException {
        return DriverManager.getConnection(...some params...);
    }
}

Luego puede extender su clase MySQLDatabaseConnectionFactorypara devolver una conexión simulada, hacer afirmaciones sobre los parámetros, etc.

La clase extendida puede residir dentro del caso de prueba, si se encuentra en el mismo paquete (lo que le animo a que haga)

public class MockedConnectionFactory extends MySQLDatabaseConnectionFactory {

    Connection _getConnection(...some params...) throws SQLException {
        if (some param != something) throw new InvalidParameterException();

        //consider mocking some methods with when(yourMock.something()).thenReturn(value)
        return Mockito.mock(Connection.class);
    }
}


6

Mockito no puede capturar métodos estáticos, pero desde Mockito 2.14.0 puede simularlo creando instancias de invocación de métodos estáticos.

Ejemplo (extraído de sus pruebas ):

public class StaticMockingExperimentTest extends TestBase {

    Foo mock = Mockito.mock(Foo.class);
    MockHandler handler = Mockito.mockingDetails(mock).getMockHandler();
    Method staticMethod;
    InvocationFactory.RealMethodBehavior realMethod = new InvocationFactory.RealMethodBehavior() {
        @Override
        public Object call() throws Throwable {
            return null;
        }
    };

    @Before
    public void before() throws Throwable {
        staticMethod = Foo.class.getDeclaredMethod("staticMethod", String.class);
    }

    @Test
    public void verify_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        handler.handle(invocation);

        //verify staticMethod on mock
        //Mockito cannot capture static methods so we will simulate this scenario in 3 steps:
        //1. Call standard 'verify' method. Internally, it will add verificationMode to the thread local state.
        //  Effectively, we indicate to Mockito that right now we are about to verify a method call on this mock.
        verify(mock);
        //2. Create the invocation instance using the new public API
        //  Mockito cannot capture static methods but we can create an invocation instance of that static invocation
        Invocation verification = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        //3. Make Mockito handle the static method invocation
        //  Mockito will find verification mode in thread local state and will try verify the invocation
        handler.handle(verification);

        //verify zero times, method with different argument
        verify(mock, times(0));
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        handler.handle(differentArg);
    }

    @Test
    public void stubbing_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "foo");
        handler.handle(invocation);

        //register stubbing
        when(null).thenReturn("hey");

        //validate stubbed return value
        assertEquals("hey", handler.handle(invocation));
        assertEquals("hey", handler.handle(invocation));

        //default null value is returned if invoked with different argument
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        assertEquals(null, handler.handle(differentArg));
    }

    static class Foo {

        private final String arg;

        public Foo(String arg) {
            this.arg = arg;
        }

        public static String staticMethod(String arg) {
            return "";
        }

        @Override
        public String toString() {
            return "foo:" + arg;
        }
    }
}

Su objetivo no es admitir directamente la burla estática, sino mejorar sus API públicas para que otras bibliotecas, como Powermockito , no tengan que depender de API internas o tengan que duplicar directamente algún código de Mockito. ( fuente )

Descargo de responsabilidad: el equipo de Mockito cree que el camino al infierno está pavimentado con métodos estáticos. Sin embargo, el trabajo de Mockito no es proteger su código de métodos estáticos. Si no le gusta que su equipo haga burlas estáticas, deje de usar Powermockito en su organización. Mockito necesita evolucionar como un juego de herramientas con una visión obstinada sobre cómo deben escribirse las pruebas de Java (por ejemplo, ¡¡no te burles de las estadísticas!). Sin embargo, Mockito no es dogmático. No queremos bloquear casos de uso no recomendados como la burla estática. Simplemente no es nuestro trabajo.



1

Dado que ese método es estático, ya tiene todo lo que necesita para usarlo, por lo que anula el propósito de burlarse. Burlarse de los métodos estáticos se considera una mala práctica.

Si intenta hacer eso, significa que hay algo mal con la forma en que desea realizar las pruebas.

Por supuesto, puede usar PowerMockito o cualquier otro marco capaz de hacerlo, pero intente repensar su enfoque.

Por ejemplo: intente burlarse / proporcionar los objetos, que ese método estático consume en su lugar.


0

Use el marco JMockit . Funcionó para mi. No tiene que escribir declaraciones para burlarse del método DBConenction.getConnection (). Solo el siguiente código es suficiente.

@Mock a continuación es el paquete mockit.Mock

Connection jdbcConnection = Mockito.mock(Connection.class);

MockUp<DBConnection> mockUp = new MockUp<DBConnection>() {

            DBConnection singleton = new DBConnection();

            @Mock
            public DBConnection getInstance() { 
                return singleton;
            }

            @Mock
            public Connection getConnection() {
                return jdbcConnection;
            }
         };
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.