Burlarse de los métodos estáticos con Mockito

Escribí una fábrica para producir objetos java.sql.Connection :

 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 ​​a DriverManager.getConnection , pero no sé cómo simular 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?

Use PowerMockito encima de Mockito.

Código de ejemplo:

 @RunWith(PowerMockRunner.class) @PrepareForTest(DriverManager.class) public class Mocker { @Test public void testName() throws Exception { //given PowerMockito.mockStatic(DriverManager.class); BDDMockito.given(DriverManager.getConnection(...)).willReturn(...); //when sut.execute(); //then PowerMockito.verifyStatic(); DriverManager.getConnection(...); } 

Más información:

  • ¿Por qué Mockito no se burla de los métodos estáticos?

La estrategia típica para esquivar los métodos estáticos que no se puede evitar usar es creando objetos envueltos y utilizando los objetos del contenedor.

Los objetos de envoltura se convierten en fachadas para las clases estáticas reales, y usted 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, por ejemplo, teniendo un constructor predeterminado para el 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í tienes una clase que puede probarse fácilmente, porque no usas 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 haz tu Wrapper bean @ApplicationScoped, haz que lo inyecten como colaborador (ni siquiera necesitas constructores desordenados para probar), y continúa con la burla.

Como se mencionó anteriormente, no puede simular 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, inyéctela mediante algún tipo de dependency injection y verifique en esa simulación.

Tuve un problema similar. La respuesta aceptada no funcionó para mí, hasta que realicé el cambio: @PrepareForTest(TheClassThatContainsStaticMethod.class) , de 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()); } } 

Para simular el método estático, debe usar una vista de Powermock: https://github.com/powermock/powermock/wiki/MockStatic . Mockito no proporciona esta funcionalidad.

Puedes leer un buen artículo sobre mockito: http://refcardz.dzone.com/refcardz/mockito

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 MySQLDatabaseConnectionFactory para devolver una conexión MySQLDatabaseConnectionFactory , hacer afirmaciones sobre los parámetros, etc.

La clase extendida puede residir dentro del caso de prueba, si está ubicada en el mismo paquete (lo que le recomiendo 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); } } 

Observación: cuando llama al método estático dentro de una entidad estática, necesita 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, necesitarí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()); 

También escribí una combinación de Mockito y AspectJ: https://github.com/iirekm/misc/tree/master/ajmock

Tu ejemplo se convierte en:

 when(() -> DriverManager.getConnection(...)).thenReturn(...); 

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 objective no es apoyar 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 piensa 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 te gusta que tu equipo haga burlas estáticas, deja de usar Powermockito en tu organización. Mockito necesita evolucionar como un conjunto de herramientas con una visión obstinada sobre cómo deben escribirse las pruebas de Java (por ejemplo, ¡no se burle de la estática!). 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.